mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-06-28 18:43:46 +01:00
Compare commits
1 Commits
v12.0-rc2
...
renovate/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec36c5aa64 |
4
.github/workflows/ci-codeql-analysis.yml
vendored
4
.github/workflows/ci-codeql-analysis.yml
vendored
@@ -24,10 +24,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
|
||||
8
.github/workflows/ci-compat.yml
vendored
8
.github/workflows/ci-compat.yml
vendored
@@ -11,13 +11,13 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
@@ -40,14 +40,14 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
|
||||
4
.github/workflows/ci-format.yml
vendored
4
.github/workflows/ci-format.yml
vendored
@@ -15,9 +15,9 @@ jobs:
|
||||
format-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
- uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
with:
|
||||
dotnet-version: ${{ env.SDK_VERSION }}
|
||||
|
||||
|
||||
4
.github/workflows/ci-tests.yml
vendored
4
.github/workflows/ci-tests.yml
vendored
@@ -20,9 +20,9 @@ jobs:
|
||||
|
||||
runs-on: "${{ matrix.os }}"
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
- uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
with:
|
||||
dotnet-version: ${{ env.SDK_VERSION }}
|
||||
|
||||
|
||||
6
.github/workflows/commands.yml
vendored
6
.github/workflows/commands.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
reactions: '+1'
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
@@ -40,12 +40,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: pull in script
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
repository: jellyfin/jellyfin-triage-script
|
||||
|
||||
- name: install python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: '3.14'
|
||||
cache: 'pip'
|
||||
|
||||
4
.github/workflows/issue-template-check.yml
vendored
4
.github/workflows/issue-template-check.yml
vendored
@@ -10,12 +10,12 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: pull in script
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
repository: jellyfin/jellyfin-triage-script
|
||||
|
||||
- name: install python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: '3.14'
|
||||
cache: 'pip'
|
||||
|
||||
4
.github/workflows/openapi-generate.yml
vendored
4
.github/workflows/openapi-generate.yml
vendored
@@ -22,13 +22,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
repository: ${{ inputs.repository }}
|
||||
|
||||
- name: Configure .NET
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
|
||||
2
.github/workflows/openapi-pull-request.yml
vendored
2
.github/workflows/openapi-pull-request.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
base_ref: ${{ steps.ancestor.outputs.base_ref }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
4
.github/workflows/release-bump-version.yaml
vendored
4
.github/workflows/release-bump-version.yaml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
yq-version: v4.9.8
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ env.TAG_BRANCH }}
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ env.TAG_BRANCH }}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
||||
<PackageVersion Include="FsCheck.Xunit.v3" Version="3.3.3" />
|
||||
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.5" />
|
||||
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="14.2.0" />
|
||||
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
|
||||
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
|
||||
<PackageVersion Include="Ignore" Version="0.2.1" />
|
||||
@@ -47,7 +47,7 @@
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.9" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
|
||||
<PackageVersion Include="MimeTypes" Version="2.5.2" />
|
||||
<PackageVersion Include="Morestachio" Version="5.0.1.670" />
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
@@ -69,14 +69,14 @@
|
||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||
<PackageVersion Include="SharpCompress" Version="0.49.1" />
|
||||
<PackageVersion Include="SharpFuzz" Version="2.3.0" />
|
||||
<PackageVersion Include="SkiaSharp" Version="3.119.4" />
|
||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.119.4" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
|
||||
<PackageVersion Include="SkiaSharp" Version="4.148.0" />
|
||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="4.148.0" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="4.148.0" />
|
||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageVersion Include="Svg.Skia" Version="3.7.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.2.3" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.2.3" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.2.1" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.2.1" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.9" />
|
||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="7.15.3" />
|
||||
|
||||
@@ -93,9 +93,6 @@ using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.System;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using MediaBrowser.Providers.Books;
|
||||
using MediaBrowser.Providers.Books.ComicBookInfo;
|
||||
using MediaBrowser.Providers.Books.ComicInfo;
|
||||
using MediaBrowser.Providers.Lyric;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
using MediaBrowser.Providers.Plugins.ListenBrainz;
|
||||
@@ -499,14 +496,6 @@ namespace Emby.Server.Implementations
|
||||
serviceCollection.AddSingleton<ListenBrainzLabsClient>();
|
||||
serviceCollection.AddSingleton<ListenBrainzSimilarArtistProvider>();
|
||||
|
||||
// register the generic local metadata provider for comic files
|
||||
serviceCollection.AddSingleton<ComicProvider>();
|
||||
|
||||
// register the actual implementations of the local metadata provider for comic files
|
||||
serviceCollection.AddSingleton<IComicProvider, ComicBookInfoProvider>();
|
||||
serviceCollection.AddSingleton<IComicProvider, ExternalComicInfoProvider>();
|
||||
serviceCollection.AddSingleton<IComicProvider, InternalComicInfoProvider>();
|
||||
|
||||
serviceCollection.AddSingleton(NetManager);
|
||||
|
||||
serviceCollection.AddSingleton<ITaskManager, TaskManager>();
|
||||
|
||||
64
Emby.Server.Implementations/Localization/Core/en_US.json
Normal file
64
Emby.Server.Implementations/Localization/Core/en_US.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"AppDeviceValues": "App: {0}, Device: {1}",
|
||||
"Artists": "Artists",
|
||||
"AuthenticationSucceededWithUserName": "{0} successfully authenticated",
|
||||
"Books": "Books",
|
||||
"ChapterNameValue": "Chapter {0}",
|
||||
"Collections": "Collections",
|
||||
"Default": "Default",
|
||||
"External": "External",
|
||||
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
|
||||
"Favorites": "Favorites",
|
||||
"Folders": "Folders",
|
||||
"Forced": "Forced",
|
||||
"Genres": "Genres",
|
||||
"HeaderContinueWatching": "Continue Watching",
|
||||
"HeaderFavoriteEpisodes": "Favorite Episodes",
|
||||
"HeaderFavoriteShows": "Favorite Shows",
|
||||
"HeaderLiveTV": "Live TV",
|
||||
"HeaderNextUp": "Next Up",
|
||||
"HearingImpaired": "Hearing Impaired",
|
||||
"HomeVideos": "Home Videos",
|
||||
"Inherit": "Inherit",
|
||||
"LabelIpAddressValue": "IP address: {0}",
|
||||
"LabelRunningTimeValue": "Running time: {0}",
|
||||
"Latest": "Latest",
|
||||
"LyricDownloadFailureFromForItem": "Lyrics failed to download from {0} for {1}",
|
||||
"MixedContent": "Mixed content",
|
||||
"Movies": "Movies",
|
||||
"Music": "Music",
|
||||
"MusicVideos": "Music Videos",
|
||||
"NameInstallFailed": "{0} installation failed",
|
||||
"NameSeasonNumber": "Season {0}",
|
||||
"NameSeasonUnknown": "Season Unknown",
|
||||
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Application update available",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Application update installed",
|
||||
"NotificationOptionAudioPlayback": "Audio playback started",
|
||||
"NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
|
||||
"NotificationOptionCameraImageUploaded": "Camera image uploaded",
|
||||
"NotificationOptionInstallationFailed": "Installation failure",
|
||||
"NotificationOptionNewLibraryContent": "New content added",
|
||||
"NotificationOptionPluginError": "Plugin failure",
|
||||
"NotificationOptionPluginInstalled": "Plugin installed",
|
||||
"NotificationOptionPluginUninstalled": "Plugin uninstalled",
|
||||
"NotificationOptionPluginUpdateInstalled": "Plugin update installed",
|
||||
"NotificationOptionServerRestartRequired": "Server restart required",
|
||||
"NotificationOptionTaskFailed": "Scheduled task failure",
|
||||
"NotificationOptionUserLockedOut": "User locked out",
|
||||
"NotificationOptionVideoPlayback": "Video playback started",
|
||||
"NotificationOptionVideoPlaybackStopped": "Video playback stopped",
|
||||
"Original": "Original",
|
||||
"Photos": "Photos",
|
||||
"PluginInstalledWithName": "{0} was installed",
|
||||
"PluginUninstalledWithName": "{0} was uninstalled",
|
||||
"PluginUpdatedWithName": "{0} was updated",
|
||||
"ScheduledTaskFailedWithName": "{0} failed",
|
||||
"Shows": "Shows",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.",
|
||||
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} to {1}",
|
||||
"TvShows": "TV Shows",
|
||||
"Undefined": "Undefined",
|
||||
"UserCreatedWithName": "User {0} has been created",
|
||||
"UserDeletedWithName": "User {0} has been deleted"
|
||||
}
|
||||
@@ -106,6 +106,5 @@
|
||||
"TaskRefreshTrickplayImages": "Xerar miniaturas de previsualización",
|
||||
"TaskAudioNormalizationDescription": "Escanea ficheiros á procura de datos de normalización de volume.",
|
||||
"CleanupUserDataTask": "Tarefa de limpeza de datos dos usuarios",
|
||||
"CleanupUserDataTaskDescription": "Limpa todos os datos do usuario (estado de visualización, de favorito etc.) dos medios ausentes polo menos 90 días.",
|
||||
"Original": "Orixinal"
|
||||
"CleanupUserDataTaskDescription": "Limpa todos os datos do usuario (estado de visualización, de favorito etc.) dos medios ausentes polo menos 90 días."
|
||||
}
|
||||
|
||||
@@ -106,7 +106,5 @@
|
||||
"TaskDownloadMissingLyrics": "누락된 가사 다운로드",
|
||||
"TaskDownloadMissingLyricsDescription": "가사 다운로드",
|
||||
"CleanupUserDataTask": "사용자 데이터 정리 작업",
|
||||
"CleanupUserDataTaskDescription": "최소 90일 이상 존재하지 않는 미디어에 대한 사용자 데이터(시청 상태, 즐겨찾기 등)를 정리합니다.",
|
||||
"LyricDownloadFailureFromForItem": "{1}에 대한 가사를 {0}에서 다운로드하지 못했습니다",
|
||||
"Original": "원본"
|
||||
"CleanupUserDataTaskDescription": "최소 90일 이상 존재하지 않는 미디어에 대한 사용자 데이터(시청 상태, 즐겨찾기 등)를 정리합니다."
|
||||
}
|
||||
|
||||
@@ -1,5 +1 @@
|
||||
{
|
||||
"Artists": "Wasanii",
|
||||
"Books": "Vitabu",
|
||||
"Collections": "Mikusanyiko"
|
||||
}
|
||||
{}
|
||||
|
||||
@@ -566,15 +566,11 @@ namespace Emby.Server.Implementations.Localization
|
||||
|
||||
private static string GetResourceFilename(string culture)
|
||||
{
|
||||
// Region codes may use a '-' (BCP-47, e.g. "pt-BR") or '_' (e.g. "es_419", "ar_SA") separator.
|
||||
// Normalize the casing (lower-case language, upper-case region) while preserving the separator
|
||||
// so the result matches the embedded resource file name, which is case-sensitive.
|
||||
var separatorIndex = culture.IndexOfAny(['-', '_']);
|
||||
var parts = culture.Split('-');
|
||||
|
||||
if (separatorIndex > 0)
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
var separator = culture[separatorIndex];
|
||||
culture = culture[..separatorIndex].ToLowerInvariant() + separator + culture[(separatorIndex + 1)..].ToUpperInvariant();
|
||||
culture = parts[0].ToLowerInvariant() + "-" + parts[1].ToUpperInvariant();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -18,7 +17,6 @@ public class OptimizeDatabaseTask : IScheduledTask, IConfigurableScheduledTask
|
||||
private readonly ILogger<OptimizeDatabaseTask> _logger;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OptimizeDatabaseTask" /> class.
|
||||
@@ -26,17 +24,14 @@ public class OptimizeDatabaseTask : IScheduledTask, IConfigurableScheduledTask
|
||||
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
||||
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||
/// <param name="jellyfinDatabaseProvider">Instance of the JellyfinDatabaseProvider that can be used for provider specific operations.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
public OptimizeDatabaseTask(
|
||||
ILogger<OptimizeDatabaseTask> logger,
|
||||
ILocalizationManager localization,
|
||||
IJellyfinDatabaseProvider jellyfinDatabaseProvider,
|
||||
ILibraryManager libraryManager)
|
||||
IJellyfinDatabaseProvider jellyfinDatabaseProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_localization = localization;
|
||||
_jellyfinDatabaseProvider = jellyfinDatabaseProvider;
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -73,15 +68,6 @@ public class OptimizeDatabaseTask : IScheduledTask, IConfigurableScheduledTask
|
||||
/// <inheritdoc />
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
// Vacuuming/checkpointing requires an exclusive lock on the database. Running it while a library scan is in
|
||||
// progress causes both operations to contend for the database and can stall the scan, so defer optimization
|
||||
// until no scan is running. The task will run again on its next trigger.
|
||||
if (_libraryManager.IsScanRunning)
|
||||
{
|
||||
_logger.LogInformation("Skipping database optimization because a library scan is currently running.");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Optimizing and vacuuming jellyfin.db...");
|
||||
|
||||
try
|
||||
|
||||
@@ -9,7 +9,6 @@ using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
|
||||
|
||||
@@ -21,7 +20,6 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory;
|
||||
private readonly ILogger<PeopleValidationTask> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PeopleValidationTask" /> class.
|
||||
@@ -29,13 +27,11 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||
/// <param name="dbContextFactory">Instance of the <see cref="IDbContextFactory{TContext}"/> interface.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{PeopleValidationTask}"/> interface.</param>
|
||||
public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization, IDbContextFactory<JellyfinDbContext> dbContextFactory, ILogger<PeopleValidationTask> logger)
|
||||
public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization, IDbContextFactory<JellyfinDbContext> dbContextFactory)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_localization = localization;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -75,18 +71,13 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
|
||||
/// <inheritdoc />
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
// People validation performs heavy database writes that contend with an active library scan.
|
||||
// Defer it until the scan has finished; the task will run again on its next trigger.
|
||||
if (_libraryManager.IsScanRunning)
|
||||
{
|
||||
_logger.LogInformation("Skipping people validation because a library scan is currently running.");
|
||||
return;
|
||||
}
|
||||
IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2));
|
||||
await _libraryManager.ValidatePeopleAsync(subProgress, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
subProgress = new Progress<double>((val) => progress.Report((val / 2) + 50));
|
||||
var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (context.ConfigureAwait(false))
|
||||
{
|
||||
IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2));
|
||||
var dupQuery = context.Peoples
|
||||
.GroupBy(e => new { e.Name, e.PersonType })
|
||||
.Where(e => e.Count() > 1)
|
||||
@@ -132,18 +123,7 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
|
||||
ArrayPool<Guid[]>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
var peopleToDelete = await context.Peoples
|
||||
.Where(p => !context.PeopleBaseItemMap.Any(m => m.PeopleId.Equals(p.Id)))
|
||||
.ExecuteDeleteAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
_logger.LogInformation("Removed {Count} orphaned people.", peopleToDelete);
|
||||
|
||||
subProgress.Report(100);
|
||||
}
|
||||
|
||||
IProgress<double> validateProgress = new Progress<double>((val) => progress.Report((val / 2) + 50));
|
||||
await _libraryManager.ValidatePeopleAsync(validateProgress, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,10 +343,6 @@ namespace Emby.Server.Implementations.Session
|
||||
_activeLiveStreamSessions.TryRemove(liveStreamId, out _);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
liveStreamNeedsToBeClosed = true;
|
||||
}
|
||||
|
||||
if (liveStreamNeedsToBeClosed)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@@ -33,8 +32,6 @@ namespace Emby.Server.Implementations.Updates
|
||||
/// </summary>
|
||||
public class InstallationManager : IInstallationManager
|
||||
{
|
||||
private static readonly SearchValues<char> InvalidPackageNameChars = SearchValues.Create([.. Path.GetInvalidFileNameChars(), '/', '\\']);
|
||||
|
||||
/// <summary>
|
||||
/// The logger.
|
||||
/// </summary>
|
||||
@@ -524,27 +521,9 @@ namespace Emby.Server.Implementations.Updates
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsValidPackageDirectoryName(package.Name))
|
||||
{
|
||||
_logger.LogError("Refusing to install package with invalid name {PackageName}.", package.Name);
|
||||
throw new InvalidDataException($"Plugin package name '{package.Name}' is not a valid directory name.");
|
||||
}
|
||||
|
||||
// Always override the passed-in target (which is a file) and figure it out again
|
||||
string targetDir = Path.Combine(_appPaths.PluginsPath, package.Name);
|
||||
|
||||
var pluginsRoot = Path.TrimEndingDirectorySeparator(Path.GetFullPath(_appPaths.PluginsPath));
|
||||
var resolvedTarget = Path.GetFullPath(targetDir);
|
||||
if (!resolvedTarget.StartsWith(pluginsRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogError(
|
||||
"Refusing to install package {PackageName}: resolved target {Resolved} is outside plugins directory {Root}.",
|
||||
package.Name,
|
||||
resolvedTarget,
|
||||
pluginsRoot);
|
||||
throw new InvalidDataException($"Plugin package name '{package.Name}' resolves outside the plugins directory.");
|
||||
}
|
||||
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.GetAsync(new Uri(package.SourceUrl), cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
@@ -593,26 +572,6 @@ namespace Emby.Server.Implementations.Updates
|
||||
_pluginManager.ImportPluginFrom(targetDir);
|
||||
}
|
||||
|
||||
private static bool IsValidPackageDirectoryName(string? name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (name.Equals(".", StringComparison.Ordinal) || name.Equals("..", StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (name.IndexOfAny(InvalidPackageNameChars) >= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
|
||||
{
|
||||
LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Id) && p.Version.Equals(package.Version))
|
||||
|
||||
@@ -1002,7 +1002,9 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
{
|
||||
if (!string.IsNullOrEmpty(pw))
|
||||
{
|
||||
listingsProviderInfo.Password = Convert.ToHexStringLower(SHA1.HashData(Encoding.UTF8.GetBytes(pw)));
|
||||
// TODO: remove ToLower when Convert.ToHexString supports lowercase
|
||||
// Schedules Direct requires the hex to be lowercase
|
||||
listingsProviderInfo.Password = Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(pw))).ToLowerInvariant();
|
||||
}
|
||||
|
||||
return await _listingsManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false);
|
||||
|
||||
@@ -12,7 +12,6 @@ using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Server.Implementations.StorageHelpers;
|
||||
using Jellyfin.Server.Implementations.SystemBackupService;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.SystemBackupService;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -34,7 +33,6 @@ public class BackupService : IBackupService
|
||||
private readonly IServerApplicationPaths _applicationPaths;
|
||||
private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider;
|
||||
private readonly IHostApplicationLifetime _hostApplicationLifetime;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private static readonly JsonSerializerOptions _serializerSettings = new JsonSerializerOptions(JsonSerializerDefaults.General)
|
||||
{
|
||||
AllowTrailingCommas = true,
|
||||
@@ -52,15 +50,13 @@ public class BackupService : IBackupService
|
||||
/// <param name="applicationPaths">The application paths.</param>
|
||||
/// <param name="jellyfinDatabaseProvider">The Jellyfin database Provider in use.</param>
|
||||
/// <param name="applicationLifetime">The SystemManager.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
public BackupService(
|
||||
ILogger<BackupService> logger,
|
||||
IDbContextFactory<JellyfinDbContext> dbProvider,
|
||||
IServerApplicationHost applicationHost,
|
||||
IServerApplicationPaths applicationPaths,
|
||||
IJellyfinDatabaseProvider jellyfinDatabaseProvider,
|
||||
IHostApplicationLifetime applicationLifetime,
|
||||
ILibraryManager libraryManager)
|
||||
IHostApplicationLifetime applicationLifetime)
|
||||
{
|
||||
_logger = logger;
|
||||
_dbProvider = dbProvider;
|
||||
@@ -68,7 +64,6 @@ public class BackupService : IBackupService
|
||||
_applicationPaths = applicationPaths;
|
||||
_jellyfinDatabaseProvider = jellyfinDatabaseProvider;
|
||||
_hostApplicationLifetime = applicationLifetime;
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -268,14 +263,6 @@ public class BackupService : IBackupService
|
||||
/// <inheritdoc/>
|
||||
public async Task<BackupManifestDto> CreateBackupAsync(BackupOptionsDto backupOptions)
|
||||
{
|
||||
// Creating a backup runs a database optimization and reads the entire database under a transaction, both of
|
||||
// which heavily contend with an active library scan and could capture an inconsistent database state.
|
||||
if (_libraryManager.IsScanRunning)
|
||||
{
|
||||
_logger.LogWarning("Cannot create a backup while a library scan is running.");
|
||||
throw new InvalidOperationException("Cannot create a backup while a library scan is running. Please try again once the scan has finished.");
|
||||
}
|
||||
|
||||
var manifest = new BackupManifest()
|
||||
{
|
||||
DateCreated = DateTime.UtcNow,
|
||||
|
||||
@@ -65,13 +65,8 @@ public class ItemPersistenceService : IItemPersistenceService
|
||||
descendantIds.Add(id);
|
||||
}
|
||||
|
||||
// Use WhereOneOrMany instead of a raw HashSet.Contains so large id sets are bound as a
|
||||
// single parameter (json_each) rather than one SQL variable per id, which would otherwise
|
||||
// overflow SQLite's variable limit when deleting many items at once (e.g. migrations).
|
||||
var ownerIds = descendantIds.ToArray();
|
||||
var extraIds = context.BaseItems
|
||||
.Where(e => e.OwnerId.HasValue)
|
||||
.WhereOneOrMany(ownerIds, e => e.OwnerId!.Value)
|
||||
.Where(e => e.OwnerId.HasValue && descendantIds.Contains(e.OwnerId.Value))
|
||||
.Select(e => e.Id)
|
||||
.ToArray();
|
||||
|
||||
|
||||
@@ -215,11 +215,8 @@ internal class JellyfinMigrationService
|
||||
logger.LogInformation("There are {Pending} migrations for stage {Stage}.", pendingCodeMigrations.Length, stage);
|
||||
migrations = pendingMigrations.OrderBy(e => e.Key).ToArray();
|
||||
|
||||
var migrationIndex = 0;
|
||||
foreach (var item in migrations)
|
||||
{
|
||||
// Surface generic "Running migration X of Y" progress in the always-visible startup UI header.
|
||||
SetupServer.ReportActivity(StartupActivity.Migration(++migrationIndex, migrations.Length));
|
||||
var migrationLogger = logger.With(_loggerFactory.CreateLogger(item.Migration.GetType().Name)).BeginGroup($"{item.Key}");
|
||||
try
|
||||
{
|
||||
|
||||
@@ -76,36 +76,25 @@ public class CleanupOrphanedExtras : IAsyncMigrationRoutine
|
||||
|
||||
_logger.LogInformation("Found {Count} orphaned extras to remove", orphanedItemIds.Count);
|
||||
|
||||
// Resolve items for metadata path cleanup, then delete in batches so we never issue one
|
||||
// massive delete transaction and progress stays visible on large libraries.
|
||||
_logger.LogInformation("Deleting {Count} orphaned extras...", orphanedItemIds.Count);
|
||||
const int deleteBatchSize = 500;
|
||||
var deletedSoFar = 0;
|
||||
for (var offset = 0; offset < orphanedItemIds.Count; offset += deleteBatchSize)
|
||||
// Batch-resolve items for metadata path cleanup, then delete all at once
|
||||
var itemsToDelete = new List<BaseItem>();
|
||||
foreach (var itemId in orphanedItemIds)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var batch = orphanedItemIds.GetRange(offset, Math.Min(deleteBatchSize, orphanedItemIds.Count - offset));
|
||||
var itemsToDelete = batch
|
||||
.Select(itemId => BaseItemMapper.DeserializeBaseItem(
|
||||
new Database.Implementations.Entities.BaseItemEntity()
|
||||
{
|
||||
Id = itemId.Id,
|
||||
Path = itemId.Path,
|
||||
Type = itemId.Type
|
||||
},
|
||||
_logger,
|
||||
null,
|
||||
true)!)
|
||||
.ToList();
|
||||
|
||||
_libraryManager.DeleteItemsUnsafeFast(itemsToDelete);
|
||||
|
||||
deletedSoFar += batch.Count;
|
||||
_logger.LogInformation("Deleting orphaned extras: {Deleted}/{Total}", deletedSoFar, orphanedItemIds.Count);
|
||||
itemsToDelete.Add(BaseItemMapper.DeserializeBaseItem(
|
||||
new Database.Implementations.Entities.BaseItemEntity()
|
||||
{
|
||||
Id = itemId.Id,
|
||||
Path = itemId.Path,
|
||||
Type = itemId.Type
|
||||
},
|
||||
_logger,
|
||||
null,
|
||||
true)!);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Successfully removed {Count} orphaned extras", orphanedItemIds.Count);
|
||||
_libraryManager.DeleteItemsUnsafeFast(itemsToDelete);
|
||||
|
||||
_logger.LogInformation("Successfully removed {Count} orphaned extras", itemsToDelete.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,38 +136,19 @@ public class FixIncorrectOwnerIdRelationships : IAsyncMigrationRoutine
|
||||
|
||||
if (allIdsToDelete.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Deleting {Count} duplicate database entries...", allIdsToDelete.Count);
|
||||
// Batch-resolve items for metadata path cleanup, then delete all at once
|
||||
var itemsToDelete = allIdsToDelete
|
||||
.Select(id => _libraryManager.GetItemById(id))
|
||||
.Where(item => item is not null)
|
||||
.ToList();
|
||||
_libraryManager.DeleteItemsUnsafeFast(itemsToDelete!);
|
||||
|
||||
// Delete in batches so progress is visible (item resolution and deletion can take a
|
||||
// long time on large libraries) and so we never issue one massive delete transaction.
|
||||
const int deleteBatchSize = 500;
|
||||
var deletedSoFar = 0;
|
||||
for (var offset = 0; offset < allIdsToDelete.Count; offset += deleteBatchSize)
|
||||
// Fall back to direct DB deletion for any items that couldn't be resolved via LibraryManager
|
||||
var deletedIds = itemsToDelete.Select(i => i!.Id).ToHashSet();
|
||||
var unresolvedIds = allIdsToDelete.Where(id => !deletedIds.Contains(id)).ToList();
|
||||
if (unresolvedIds.Count > 0)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var batchIds = allIdsToDelete.GetRange(offset, Math.Min(deleteBatchSize, allIdsToDelete.Count - offset));
|
||||
|
||||
// Resolve items for metadata path cleanup, then delete this batch
|
||||
var itemsToDelete = batchIds
|
||||
.Select(id => _libraryManager.GetItemById(id))
|
||||
.Where(item => item is not null)
|
||||
.ToList();
|
||||
if (itemsToDelete.Count > 0)
|
||||
{
|
||||
_libraryManager.DeleteItemsUnsafeFast(itemsToDelete!);
|
||||
}
|
||||
|
||||
// Fall back to direct DB deletion for any items that couldn't be resolved via LibraryManager
|
||||
var deletedIds = itemsToDelete.Select(i => i!.Id).ToHashSet();
|
||||
var unresolvedIds = batchIds.Where(id => !deletedIds.Contains(id)).ToList();
|
||||
if (unresolvedIds.Count > 0)
|
||||
{
|
||||
_persistenceService.DeleteItem(unresolvedIds);
|
||||
}
|
||||
|
||||
deletedSoFar += batchIds.Count;
|
||||
_logger.LogInformation("Deleting duplicates: {Deleted}/{Total} items", deletedSoFar, allIdsToDelete.Count);
|
||||
_persistenceService.DeleteItem(unresolvedIds);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -182,35 +182,23 @@ public class MergeDuplicateMusicArtists : IAsyncMigrationRoutine
|
||||
// Resolve via LibraryManager so DeleteItemsUnsafeFast can also remove the
|
||||
// %MetadataPath%/artists/<Name> directories that the duplicate stubs left behind.
|
||||
// Fall back to the persistence service for any items the LibraryManager can't resolve.
|
||||
// Delete in batches so we never issue one massive delete transaction and progress stays visible.
|
||||
_logger.LogInformation("Deleting {Count} duplicate MusicArtist records...", idsToDelete.Count);
|
||||
const int deleteBatchSize = 500;
|
||||
var deletedSoFar = 0;
|
||||
for (var offset = 0; offset < idsToDelete.Count; offset += deleteBatchSize)
|
||||
var itemsToDelete = idsToDelete
|
||||
.Select(id => _libraryManager.GetItemById(id))
|
||||
.Where(item => item is not null)
|
||||
.ToList();
|
||||
if (itemsToDelete.Count > 0)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var batchIds = idsToDelete.GetRange(offset, Math.Min(deleteBatchSize, idsToDelete.Count - offset));
|
||||
|
||||
var itemsToDelete = batchIds
|
||||
.Select(id => _libraryManager.GetItemById(id))
|
||||
.Where(item => item is not null)
|
||||
.ToList();
|
||||
if (itemsToDelete.Count > 0)
|
||||
{
|
||||
_libraryManager.DeleteItemsUnsafeFast(itemsToDelete!);
|
||||
}
|
||||
|
||||
var deletedIds = itemsToDelete.Select(i => i!.Id).ToHashSet();
|
||||
var unresolvedIds = batchIds.Where(id => !deletedIds.Contains(id)).ToList();
|
||||
if (unresolvedIds.Count > 0)
|
||||
{
|
||||
_persistenceService.DeleteItem(unresolvedIds);
|
||||
}
|
||||
|
||||
deletedSoFar += batchIds.Count;
|
||||
_logger.LogInformation("Deleting duplicate MusicArtist records: {Deleted}/{Total}", deletedSoFar, idsToDelete.Count);
|
||||
_libraryManager.DeleteItemsUnsafeFast(itemsToDelete!);
|
||||
}
|
||||
|
||||
var deletedIds = itemsToDelete.Select(i => i!.Id).ToHashSet();
|
||||
var unresolvedIds = idsToDelete.Where(id => !deletedIds.Contains(id)).ToList();
|
||||
if (unresolvedIds.Count > 0)
|
||||
{
|
||||
_persistenceService.DeleteItem(unresolvedIds);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Removed {Count} duplicate MusicArtist records.", idsToDelete.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,35 +184,23 @@ public class MergeDuplicatePeople : IAsyncMigrationRoutine
|
||||
|
||||
// Resolve via LibraryManager so DeleteItemsUnsafeFast can also remove the
|
||||
// %MetadataPath%/People/<Letter>/<Name> directories the duplicate stubs left behind.
|
||||
// Delete in batches so we never issue one massive delete transaction and progress stays visible.
|
||||
_logger.LogInformation("Deleting {Count} duplicate Person BaseItems...", idsToDelete.Count);
|
||||
const int deleteBatchSize = 500;
|
||||
var deletedSoFar = 0;
|
||||
for (var offset = 0; offset < idsToDelete.Count; offset += deleteBatchSize)
|
||||
var itemsToDelete = idsToDelete
|
||||
.Select(id => _libraryManager.GetItemById(id))
|
||||
.Where(item => item is not null)
|
||||
.ToList();
|
||||
if (itemsToDelete.Count > 0)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var batchIds = idsToDelete.GetRange(offset, Math.Min(deleteBatchSize, idsToDelete.Count - offset));
|
||||
|
||||
var itemsToDelete = batchIds
|
||||
.Select(id => _libraryManager.GetItemById(id))
|
||||
.Where(item => item is not null)
|
||||
.ToList();
|
||||
if (itemsToDelete.Count > 0)
|
||||
{
|
||||
_libraryManager.DeleteItemsUnsafeFast(itemsToDelete!);
|
||||
}
|
||||
|
||||
var deletedIds = itemsToDelete.Select(i => i!.Id).ToHashSet();
|
||||
var unresolvedIds = batchIds.Where(id => !deletedIds.Contains(id)).ToList();
|
||||
if (unresolvedIds.Count > 0)
|
||||
{
|
||||
_persistenceService.DeleteItem(unresolvedIds);
|
||||
}
|
||||
|
||||
deletedSoFar += batchIds.Count;
|
||||
_logger.LogInformation("Deleting duplicate Person BaseItems: {Deleted}/{Total}", deletedSoFar, idsToDelete.Count);
|
||||
_libraryManager.DeleteItemsUnsafeFast(itemsToDelete!);
|
||||
}
|
||||
|
||||
var deletedIds = itemsToDelete.Select(i => i!.Id).ToHashSet();
|
||||
var unresolvedIds = idsToDelete.Where(id => !deletedIds.Contains(id)).ToList();
|
||||
if (unresolvedIds.Count > 0)
|
||||
{
|
||||
_persistenceService.DeleteItem(unresolvedIds);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Removed {Count} duplicate Person BaseItems.", idsToDelete.Count);
|
||||
}
|
||||
|
||||
private async Task MergePeoplesRowsAsync(JellyfinDbContext context, CancellationToken cancellationToken)
|
||||
|
||||
@@ -133,12 +133,10 @@ namespace Jellyfin.Server
|
||||
}
|
||||
}
|
||||
|
||||
SetupServer.ReportActivity(StartupActivity.CheckingStorage);
|
||||
StorageHelper.TestCommonPathsForStorageCapacity(appPaths, StartupLogger.Logger.With(_loggerFactory.CreateLogger<Startup>()).BeginGroup($"Storage Check"));
|
||||
|
||||
StartupHelpers.PerformStaticInitialization();
|
||||
|
||||
SetupServer.ReportActivity(StartupActivity.Initializing);
|
||||
await ApplyStartupMigrationAsync(appPaths, startupConfig, options).ConfigureAwait(false);
|
||||
|
||||
do
|
||||
@@ -197,7 +195,6 @@ namespace Jellyfin.Server
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_restoreFromBackup))
|
||||
{
|
||||
SetupServer.ReportActivity(StartupActivity.RestoringBackup);
|
||||
await appHost.ServiceProvider.GetService<IBackupService>()!.RestoreBackupAsync(_restoreFromBackup).ConfigureAwait(false);
|
||||
_restoreFromBackup = null;
|
||||
_restartOnShutdown = true;
|
||||
@@ -205,13 +202,9 @@ namespace Jellyfin.Server
|
||||
}
|
||||
|
||||
var jellyfinMigrationService = ActivatorUtilities.CreateInstance<JellyfinMigrationService>(appHost.ServiceProvider);
|
||||
SetupServer.ReportActivity(StartupActivity.PreparingMigrations);
|
||||
await jellyfinMigrationService.PrepareSystemForMigration(_logger).ConfigureAwait(false);
|
||||
// "Preparing migrations" carries through the DB read; per-migration progress is reported
|
||||
// as "Running migration X of Y" from inside the step once the pending set is known.
|
||||
await jellyfinMigrationService.MigrateStepAsync(JellyfinMigrationStageTypes.CoreInitialisation, appHost.ServiceProvider).ConfigureAwait(false);
|
||||
|
||||
SetupServer.ReportActivity(StartupActivity.InitializingServices);
|
||||
await appHost.InitializeServices(startupConfig).ConfigureAwait(false);
|
||||
_appHost = appHost;
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ using Jellyfin.Server.Extensions;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.System;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
@@ -24,6 +25,9 @@ using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Morestachio;
|
||||
using Morestachio.Framework.IO.SingleStream;
|
||||
using Morestachio.Rendering;
|
||||
using Serilog;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
|
||||
@@ -40,8 +44,7 @@ public sealed class SetupServer : IDisposable
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly IConfiguration _startupConfiguration;
|
||||
private readonly ServerConfigurationManager _configurationManager;
|
||||
private static volatile string _currentActivity = StartupActivity.Starting;
|
||||
private StartupUiRenderer? _startupUiRenderer;
|
||||
private IRenderer? _startupUiRenderer;
|
||||
private IHost? _startupServer;
|
||||
private bool _disposed;
|
||||
private bool _isUnhealthy;
|
||||
@@ -73,12 +76,6 @@ public sealed class SetupServer : IDisposable
|
||||
|
||||
internal static ConcurrentQueue<StartupLogTopic>? LogQueue { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a generic, non-identifying summary of what startup is currently doing. This is shown in the
|
||||
/// always-visible header of the startup UI to unauthenticated clients, so it never contains server specific details.
|
||||
/// </summary>
|
||||
internal static string CurrentActivity => _currentActivity;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether Startup server is currently running.
|
||||
/// </summary>
|
||||
@@ -90,9 +87,64 @@ public sealed class SetupServer : IDisposable
|
||||
/// <returns>A Task.</returns>
|
||||
public async Task RunAsync()
|
||||
{
|
||||
ReportActivity(StartupActivity.Starting);
|
||||
_startupUiRenderer = await StartupUiRenderer.CreateAsync(
|
||||
Path.Combine(AppContext.BaseDirectory, "ServerSetupApp", "index.mstemplate.html")).ConfigureAwait(false);
|
||||
var fileTemplate = await File.ReadAllTextAsync(Path.Combine(AppContext.BaseDirectory, "ServerSetupApp", "index.mstemplate.html")).ConfigureAwait(false);
|
||||
_startupUiRenderer = (await ParserOptionsBuilder.New()
|
||||
.WithTemplate(fileTemplate)
|
||||
.WithFormatter(
|
||||
(Version version, int arg) =>
|
||||
{
|
||||
// version type does not for some stupid reason implement IFormattable which morestachio relies on for ToString support therefor we need to do it manually.
|
||||
return version.ToString(arg);
|
||||
},
|
||||
"ToString")
|
||||
.WithFormatter(
|
||||
(StartupLogTopic logEntry, IEnumerable<StartupLogTopic> children) =>
|
||||
{
|
||||
if (children.Any())
|
||||
{
|
||||
var maxLevel = logEntry.LogLevel;
|
||||
var stack = new Stack<StartupLogTopic>(children);
|
||||
|
||||
while (maxLevel != LogLevel.Error && stack.Count > 0 && (logEntry = stack.Pop()) is not null) // error is the highest inherted error level.
|
||||
{
|
||||
maxLevel = maxLevel < logEntry.LogLevel ? logEntry.LogLevel : maxLevel;
|
||||
foreach (var child in logEntry.Children)
|
||||
{
|
||||
stack.Push(child);
|
||||
}
|
||||
}
|
||||
|
||||
return maxLevel;
|
||||
}
|
||||
|
||||
return logEntry.LogLevel;
|
||||
},
|
||||
"FormatLogLevel")
|
||||
.WithFormatter(
|
||||
(LogLevel logLevel) =>
|
||||
{
|
||||
switch (logLevel)
|
||||
{
|
||||
case LogLevel.Trace:
|
||||
case LogLevel.Debug:
|
||||
case LogLevel.None:
|
||||
return "success";
|
||||
case LogLevel.Information:
|
||||
return "info";
|
||||
case LogLevel.Warning:
|
||||
return "warn";
|
||||
case LogLevel.Error:
|
||||
return "danger";
|
||||
case LogLevel.Critical:
|
||||
return "danger-strong";
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
},
|
||||
"ToString")
|
||||
.BuildAndParseAsync()
|
||||
.ConfigureAwait(false))
|
||||
.CreateCompiledRenderer();
|
||||
|
||||
ThrowIfDisposed();
|
||||
var retryAfterValue = TimeSpan.FromSeconds(5);
|
||||
@@ -205,14 +257,13 @@ public sealed class SetupServer : IDisposable
|
||||
new Dictionary<string, object>()
|
||||
{
|
||||
{ "isInReportingMode", _isUnhealthy },
|
||||
{ "currentActivity", CurrentActivity },
|
||||
{ "retryValue", retryAfterValue },
|
||||
{ "version", version },
|
||||
{ "logs", startupLogEntries },
|
||||
{ "networkManagerReady", networkManager is not null },
|
||||
{ "localNetworkRequest", networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress) }
|
||||
},
|
||||
context.Response.BodyWriter.AsStream())
|
||||
new ByteCounterStream(context.Response.BodyWriter.AsStream(), IODefaults.FileStreamBufferSize, true, _startupUiRenderer.ParserOptions))
|
||||
.ConfigureAwait(false);
|
||||
});
|
||||
});
|
||||
@@ -258,16 +309,6 @@ public sealed class SetupServer : IDisposable
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reports the current startup activity shown to all clients in the startup UI header.
|
||||
/// Only pass generic, non-identifying text from <see cref="StartupActivity"/>.
|
||||
/// </summary>
|
||||
/// <param name="activity">A generic description such as <see cref="StartupActivity.PreparingMigrations"/>.</param>
|
||||
internal static void ReportActivity(string activity)
|
||||
{
|
||||
_currentActivity = activity;
|
||||
}
|
||||
|
||||
internal void SoftStop()
|
||||
{
|
||||
_isUnhealthy = true;
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace Jellyfin.Server.ServerSetupApp;
|
||||
|
||||
/// <summary>
|
||||
/// A curated vocabulary of generic, non-identifying descriptions of what the server is doing during startup.
|
||||
/// These are shown in the always-visible header of the startup UI to <b>unauthenticated</b> clients, so every
|
||||
/// value must stay generic and must never contain server specific details (paths, names, plugin or migration ids, counts of items, etc.).
|
||||
/// </summary>
|
||||
public static class StartupActivity
|
||||
{
|
||||
/// <summary>The default state before any work has been reported.</summary>
|
||||
public const string Starting = "Starting up";
|
||||
|
||||
/// <summary>Validating that the configured storage locations are usable.</summary>
|
||||
public const string CheckingStorage = "Checking storage";
|
||||
|
||||
/// <summary>Bringing up the migration subsystem and running early startup checks.</summary>
|
||||
public const string Initializing = "Initializing server";
|
||||
|
||||
/// <summary>Preparing the system for migrations (e.g. taking safety backups).</summary>
|
||||
public const string PreparingMigrations = "Preparing migrations";
|
||||
|
||||
/// <summary>Restoring from a backup.</summary>
|
||||
public const string RestoringBackup = "Restoring backup";
|
||||
|
||||
/// <summary>Bringing up core services and plugins.</summary>
|
||||
public const string InitializingServices = "Initializing services";
|
||||
|
||||
/// <summary>Running the final startup tasks.</summary>
|
||||
public const string FinishingStartup = "Finishing startup";
|
||||
|
||||
/// <summary>
|
||||
/// Builds a generic "Running migration X of Y" description. Only the numeric position and total are exposed.
|
||||
/// </summary>
|
||||
/// <param name="current">The 1-based index of the migration currently running.</param>
|
||||
/// <param name="total">The total number of migrations in this batch.</param>
|
||||
/// <returns>A generic progress description.</returns>
|
||||
public static string Migration(int current, int total)
|
||||
=> string.Format(CultureInfo.InvariantCulture, "Running migration {0} of {1}", current, total);
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Morestachio;
|
||||
using Morestachio.Framework.IO.SingleStream;
|
||||
using Morestachio.Rendering;
|
||||
|
||||
namespace Jellyfin.Server.ServerSetupApp;
|
||||
|
||||
/// <summary>
|
||||
/// Compiles and renders the startup UI Morestachio template.
|
||||
/// Shared by the live <see cref="SetupServer"/> and the standalone startup UI preview tool so both
|
||||
/// exercise the exact same template and formatters.
|
||||
/// </summary>
|
||||
public sealed class StartupUiRenderer
|
||||
{
|
||||
private readonly IRenderer _renderer;
|
||||
|
||||
private StartupUiRenderer(IRenderer renderer)
|
||||
{
|
||||
_renderer = renderer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compiles the startup UI template located at <paramref name="templatePath"/>.
|
||||
/// </summary>
|
||||
/// <param name="templatePath">The full path to the <c>index.mstemplate.html</c> template.</param>
|
||||
/// <returns>A ready to use <see cref="StartupUiRenderer"/>.</returns>
|
||||
public static async Task<StartupUiRenderer> CreateAsync(string templatePath)
|
||||
{
|
||||
var fileTemplate = await File.ReadAllTextAsync(templatePath).ConfigureAwait(false);
|
||||
var renderer = (await ParserOptionsBuilder.New()
|
||||
.WithTemplate(fileTemplate)
|
||||
.WithFormatter(
|
||||
(Version version, int arg) =>
|
||||
{
|
||||
// version type does not for some stupid reason implement IFormattable which morestachio relies on for ToString support therefor we need to do it manually.
|
||||
return version.ToString(arg);
|
||||
},
|
||||
"ToString")
|
||||
.WithFormatter(
|
||||
(StartupLogTopic logEntry, IEnumerable<StartupLogTopic> children) =>
|
||||
{
|
||||
if (children.Any())
|
||||
{
|
||||
var maxLevel = logEntry.LogLevel;
|
||||
var stack = new Stack<StartupLogTopic>(children);
|
||||
|
||||
while (maxLevel != LogLevel.Error && stack.Count > 0 && (logEntry = stack.Pop()) is not null) // error is the highest inherted error level.
|
||||
{
|
||||
maxLevel = maxLevel < logEntry.LogLevel ? logEntry.LogLevel : maxLevel;
|
||||
foreach (var child in logEntry.Children)
|
||||
{
|
||||
stack.Push(child);
|
||||
}
|
||||
}
|
||||
|
||||
return maxLevel;
|
||||
}
|
||||
|
||||
return logEntry.LogLevel;
|
||||
},
|
||||
"FormatLogLevel")
|
||||
.WithFormatter(
|
||||
(LogLevel logLevel) =>
|
||||
{
|
||||
switch (logLevel)
|
||||
{
|
||||
case LogLevel.Trace:
|
||||
case LogLevel.Debug:
|
||||
case LogLevel.None:
|
||||
return "success";
|
||||
case LogLevel.Information:
|
||||
return "info";
|
||||
case LogLevel.Warning:
|
||||
return "warn";
|
||||
case LogLevel.Error:
|
||||
return "danger";
|
||||
case LogLevel.Critical:
|
||||
return "danger-strong";
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
},
|
||||
"ToString")
|
||||
.BuildAndParseAsync()
|
||||
.ConfigureAwait(false))
|
||||
.CreateCompiledRenderer();
|
||||
|
||||
return new StartupUiRenderer(renderer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders the template with the provided model into the target stream.
|
||||
/// </summary>
|
||||
/// <param name="model">The values made available to the template.</param>
|
||||
/// <param name="output">The stream the rendered HTML is written to.</param>
|
||||
/// <returns>A Task.</returns>
|
||||
public Task RenderAsync(IDictionary<string, object> model, Stream output)
|
||||
{
|
||||
return _renderer.RenderAsync(
|
||||
model,
|
||||
new ByteCounterStream(output, IODefaults.FileStreamBufferSize, true, _renderer.ParserOptions));
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
23
MediaBrowser.Providers/Books/ComicServiceRegistrator.cs
Normal file
23
MediaBrowser.Providers/Books/ComicServiceRegistrator.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Providers.Books.ComicBookInfo;
|
||||
using MediaBrowser.Providers.Books.ComicInfo;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace MediaBrowser.Providers.Books;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class ComicServiceRegistrator : IPluginServiceRegistrator
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost)
|
||||
{
|
||||
// register the generic local metadata provider for comic files
|
||||
serviceCollection.AddSingleton<ComicProvider>();
|
||||
|
||||
// register the actual implementations of the local metadata provider for comic files
|
||||
serviceCollection.AddSingleton<IComicProvider, ComicBookInfoProvider>();
|
||||
serviceCollection.AddSingleton<IComicProvider, ExternalComicInfoProvider>();
|
||||
serviceCollection.AddSingleton<IComicProvider, InternalComicInfoProvider>();
|
||||
}
|
||||
}
|
||||
@@ -549,7 +549,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
var candidateUnsynchronizedLyric = supportedLyrics.FirstOrDefault(l => l.Format is LyricsInfo.LyricsFormat.UNSYNCHRONIZED or LyricsInfo.LyricsFormat.OTHER && l.UnsynchronizedLyrics is not null);
|
||||
var lyrics = candidateSynchronizedLyric is not null ? candidateSynchronizedLyric.FormatSynch() : candidateUnsynchronizedLyric?.UnsynchronizedLyrics;
|
||||
if (!string.IsNullOrWhiteSpace(lyrics)
|
||||
&& (tryExtractEmbeddedLyrics || options.ReplaceAllMetadata))
|
||||
&& tryExtractEmbeddedLyrics)
|
||||
{
|
||||
await _lyricManager.SaveLyricAsync(audio, "lrc", lyrics).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using Jellyfin.LiveTv;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
@@ -1110,8 +1109,9 @@ namespace Jellyfin.LiveTv.Channels
|
||||
item.Path = mediaSource?.Path;
|
||||
}
|
||||
|
||||
if (LiveTvChannelImageHelper.UpdateChannelImageIfNeeded(item, null, info.ImageUrl))
|
||||
if (!string.IsNullOrEmpty(info.ImageUrl) && !item.HasImage(ImageType.Primary))
|
||||
{
|
||||
item.SetImagePath(ImageType.Primary, info.ImageUrl);
|
||||
_logger.LogDebug("Forcing update due to ImageUrl {0}", item.Name);
|
||||
forceUpdate = true;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.LiveTv;
|
||||
using Jellyfin.LiveTv.Configuration;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
@@ -449,9 +448,23 @@ public class GuideManager : IGuideManager
|
||||
|
||||
item.Name = channelInfo.Name;
|
||||
|
||||
if (LiveTvChannelImageHelper.UpdateChannelImageIfNeeded(item, channelInfo.ImagePath, channelInfo.ImageUrl))
|
||||
var currentPrimary = item.GetImageInfo(ImageType.Primary, 0);
|
||||
var imageUrlIsNull = string.IsNullOrWhiteSpace(channelInfo.ImageUrl);
|
||||
|
||||
// Update channel image if image URL has changed
|
||||
if (currentPrimary is null
|
||||
|| (!imageUrlIsNull && !string.Equals(currentPrimary.Path, channelInfo.ImageUrl, StringComparison.Ordinal)))
|
||||
{
|
||||
forceUpdate = true;
|
||||
if (!string.IsNullOrWhiteSpace(channelInfo.ImagePath))
|
||||
{
|
||||
item.SetImagePath(ImageType.Primary, channelInfo.ImagePath);
|
||||
forceUpdate = true;
|
||||
}
|
||||
else if (!imageUrlIsNull)
|
||||
{
|
||||
item.SetImagePath(ImageType.Primary, channelInfo.ImageUrl);
|
||||
forceUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
|
||||
@@ -748,7 +748,9 @@ namespace Jellyfin.LiveTv.Listings
|
||||
#pragma warning disable CA5350 // SchedulesDirect is always SHA1.
|
||||
var hashedPasswordBytes = SHA1.HashData(Encoding.ASCII.GetBytes(password));
|
||||
#pragma warning restore CA5350
|
||||
string hashedPassword = Convert.ToHexStringLower(hashedPasswordBytes);
|
||||
// TODO: remove ToLower when Convert.ToHexString supports lowercase
|
||||
// Schedules Direct requires the hex to be lowercase
|
||||
string hashedPassword = Convert.ToHexString(hashedPasswordBytes).ToLowerInvariant();
|
||||
options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
|
||||
|
||||
var root = await Request<TokenDto>(options, false, null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace Jellyfin.LiveTv;
|
||||
|
||||
/// <summary>
|
||||
/// Helpers for keeping Live TV channel icons in sync with guide data.
|
||||
/// </summary>
|
||||
internal static class LiveTvChannelImageHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies the channel icon from guide or tuner metadata.
|
||||
/// Called on each guide refresh so remote icons are re-downloaded even when the URL is unchanged.
|
||||
/// </summary>
|
||||
/// <param name="item">The channel item.</param>
|
||||
/// <param name="imagePath">The local image path from the tuner, if any.</param>
|
||||
/// <param name="imageUrl">The remote image URL from the guide provider, if any.</param>
|
||||
/// <returns><c>true</c> when the item image metadata was updated.</returns>
|
||||
internal static bool UpdateChannelImageIfNeeded(BaseItem item, string? imagePath, string? imageUrl)
|
||||
{
|
||||
var newImageSource = !string.IsNullOrWhiteSpace(imagePath)
|
||||
? imagePath
|
||||
: imageUrl;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newImageSource))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
item.SetImagePath(ImageType.Primary, newImageSource);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using Jellyfin.LiveTv;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.LiveTv.Tests;
|
||||
|
||||
public class LiveTvChannelImageHelperTests
|
||||
{
|
||||
[Fact]
|
||||
public void UpdateChannelImageIfNeeded_NoSource_DoesNotUpdate()
|
||||
{
|
||||
var channel = new LiveTvChannel { Name = "Test Channel" };
|
||||
|
||||
var updated = LiveTvChannelImageHelper.UpdateChannelImageIfNeeded(channel, null, null);
|
||||
|
||||
Assert.False(updated);
|
||||
Assert.False(channel.HasImage(ImageType.Primary));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateChannelImageIfNeeded_WithUrl_AppliesUrl()
|
||||
{
|
||||
var channel = new LiveTvChannel { Name = "Test Channel" };
|
||||
|
||||
var updated = LiveTvChannelImageHelper.UpdateChannelImageIfNeeded(
|
||||
channel,
|
||||
null,
|
||||
"https://example.com/icon.png");
|
||||
|
||||
Assert.True(updated);
|
||||
Assert.True(channel.HasImage(ImageType.Primary));
|
||||
Assert.Equal("https://example.com/icon.png", channel.GetImagePath(ImageType.Primary));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateChannelImageIfNeeded_SameUrl_StillUpdates()
|
||||
{
|
||||
var channel = new LiveTvChannel { Name = "Test Channel" };
|
||||
LiveTvChannelImageHelper.UpdateChannelImageIfNeeded(channel, null, "https://example.com/icon.png");
|
||||
|
||||
var updated = LiveTvChannelImageHelper.UpdateChannelImageIfNeeded(
|
||||
channel,
|
||||
null,
|
||||
"https://example.com/icon.png");
|
||||
|
||||
Assert.True(updated);
|
||||
Assert.Equal("https://example.com/icon.png", channel.GetImagePath(ImageType.Primary));
|
||||
}
|
||||
}
|
||||
@@ -344,20 +344,6 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
|
||||
Assert.NotEqual("Default", translated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetLocalizedString_WithBcp47NormalizationToUppercaseRegion_ReturnsTranslation()
|
||||
{
|
||||
var localizationManager = Setup(new ServerConfiguration
|
||||
{
|
||||
UICulture = "en-US"
|
||||
});
|
||||
|
||||
// he-IL normalizes to the underscore resource he_IL. The resource lookup is case-sensitive,
|
||||
// so the region casing has to be preserved or the file is not found and we fall back to en-US.
|
||||
var translated = localizationManager.GetLocalizedString("Books", "he-IL");
|
||||
Assert.Equal("ספרים", translated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetServerLocalizedString_UsesServerCulture()
|
||||
{
|
||||
|
||||
@@ -109,29 +109,5 @@ namespace Jellyfin.Server.Implementations.Tests.Updates
|
||||
var ex = await Record.ExceptionAsync(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("../evil")]
|
||||
[InlineData("..\\evil")]
|
||||
[InlineData("../../escape_attempt")]
|
||||
[InlineData("..")]
|
||||
[InlineData(".")]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData("foo/bar")]
|
||||
[InlineData("foo\\bar")]
|
||||
[InlineData("/absolute")]
|
||||
[InlineData("foo\0bar")]
|
||||
public async Task InstallPackage_InvalidName_ThrowsInvalidDataException(string name)
|
||||
{
|
||||
var packageInfo = new InstallationInfo()
|
||||
{
|
||||
Name = name,
|
||||
SourceUrl = "https://repo.jellyfin.org/releases/plugin/empty/empty.zip",
|
||||
Checksum = "11b5b2f1a9ebc4f66d6ef19018543361"
|
||||
};
|
||||
|
||||
await Assert.ThrowsAsync<InvalidDataException>(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user