Merge branch 'master' into clean-orphaned-people

This commit is contained in:
Cody Robibero
2026-06-27 10:02:33 -04:00
committed by GitHub
36 changed files with 1123 additions and 292 deletions

View File

@@ -93,6 +93,9 @@ 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;
@@ -496,6 +499,14 @@ 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>();

View File

@@ -57,6 +57,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null;
}
if (args.Parent is not null && args.Parent.IsRoot)
{
return null;
}
var seriesInfo = Naming.TV.SeriesResolver.Resolve(_namingOptions, args.Path);
var collectionType = args.GetCollectionType();

View File

@@ -0,0 +1,19 @@
{
"Books": "Kitablar",
"HomeVideos": "Ev Videoları",
"Latest": "Ən son",
"MixedContent": "Qarışıq məzmun",
"Movies": "Filmlər",
"Music": "Musiqi",
"MusicVideos": "Musiqi Videoları",
"NameSeasonUnknown": "Mövsüm Naməlum",
"NewVersionIsAvailable": "Jellyfin Serverin yeni versiyası yükləmək üçün əlçatandır.",
"NotificationOptionApplicationUpdateAvailable": "Tətbiq yeniləməsi mövcuddur",
"NotificationOptionApplicationUpdateInstalled": "Tətbiq yeniləməsi quraşdırılıb",
"NotificationOptionAudioPlayback": "Audio oxutma başladı",
"NotificationOptionAudioPlaybackStopped": "Audio oxutma dayandırıldı",
"NotificationOptionCameraImageUploaded": "Kamera şəkli yükləndi",
"NotificationOptionInstallationFailed": "Quraşdırma uğursuzluğu",
"NotificationOptionNewLibraryContent": "Yeni məzmun əlavə edildi",
"NotificationOptionPluginError": "Plugin uğursuzluğu"
}

View File

@@ -106,5 +106,6 @@
"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."
"CleanupUserDataTaskDescription": "Limpa todos os datos do usuario (estado de visualización, de favorito etc.) dos medios ausentes polo menos 90 días.",
"Original": "Orixinal"
}

View File

@@ -106,5 +106,7 @@
"TaskDownloadMissingLyrics": "누락된 가사 다운로드",
"TaskDownloadMissingLyricsDescription": "가사 다운로드",
"CleanupUserDataTask": "사용자 데이터 정리 작업",
"CleanupUserDataTaskDescription": "최소 90일 이상 존재하지 않는 미디어에 대한 사용자 데이터(시청 상태, 즐겨찾기 등)를 정리합니다."
"CleanupUserDataTaskDescription": "최소 90일 이상 존재하지 않는 미디어에 대한 사용자 데이터(시청 상태, 즐겨찾기 등)를 정리합니다.",
"LyricDownloadFailureFromForItem": "{1}에 대한 가사를 {0}에서 다운로드하지 못했습니다",
"Original": "원본"
}

View File

@@ -107,5 +107,6 @@
"TaskMoveTrickplayImagesDescription": "Move os ficheiros trickplay existentes de acordo com as definições da mediateca.",
"CleanupUserDataTaskDescription": "Apaga todos os dados de utilizador (estados de reprodução, favoritos, etc) de arquivos média não presentes há 90 dias ou mais.",
"CleanupUserDataTask": "Limpeza de dados de utilizador",
"Original": "Original"
"Original": "Original",
"LyricDownloadFailureFromForItem": "Erro ao descarregar letras de {0} para {1}"
}

View File

@@ -106,5 +106,7 @@
"TaskDownloadMissingLyrics": "Stiahnuť chýbajúce texty piesní",
"TaskDownloadMissingLyricsDescription": "Stiahne texty pre piesne",
"CleanupUserDataTask": "Prečistiť používateľské dáta",
"CleanupUserDataTaskDescription": "Vyčistí všetky dáta používateľa (stav sledovania, stav obľúbených atď.) z médií, ktoré už neexistujú aspoň 90 dní."
"CleanupUserDataTaskDescription": "Vyčistí všetky dáta používateľa (stav sledovania, stav obľúbených atď.) z médií, ktoré už neexistujú aspoň 90 dní.",
"LyricDownloadFailureFromForItem": "Text piesne sa nepodarilo stiahnuť z {0} pre {1}",
"Original": "Originál"
}

View File

@@ -106,5 +106,7 @@
"TaskAudioNormalization": "Normalizacija zvoka",
"TaskAudioNormalizationDescription": "Pregled datotek za podatke o normalizaciji zvoka.",
"CleanupUserDataTask": "Čiščenje uporabniških podatkov",
"CleanupUserDataTaskDescription": "Izbriše vse uporabniške podatke (stanje ogleda, priljubljene itd.) za vsebine, ki že več kot 90 dni niso na voljo."
"CleanupUserDataTaskDescription": "Izbriše vse uporabniške podatke (stanje ogleda, priljubljene itd.) za vsebine, ki že več kot 90 dni niso na voljo.",
"LyricDownloadFailureFromForItem": "Besedila ni bilo mogoče prenesti iz {0} za {1}",
"Original": "Original"
}

View File

@@ -566,11 +566,15 @@ namespace Emby.Server.Implementations.Localization
private static string GetResourceFilename(string culture)
{
var parts = culture.Split('-');
// 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(['-', '_']);
if (parts.Length == 2)
if (separatorIndex > 0)
{
culture = parts[0].ToLowerInvariant() + "-" + parts[1].ToUpperInvariant();
var separator = culture[separatorIndex];
culture = culture[..separatorIndex].ToLowerInvariant() + separator + culture[(separatorIndex + 1)..].ToUpperInvariant();
}
else
{

View File

@@ -3,6 +3,7 @@ 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;
@@ -17,6 +18,7 @@ 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.
@@ -24,14 +26,17 @@ 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)
IJellyfinDatabaseProvider jellyfinDatabaseProvider,
ILibraryManager libraryManager)
{
_logger = logger;
_localization = localization;
_jellyfinDatabaseProvider = jellyfinDatabaseProvider;
_libraryManager = libraryManager;
}
/// <inheritdoc />
@@ -68,6 +73,15 @@ 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

View File

@@ -75,6 +75,14 @@ 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;
}
var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{

View File

@@ -343,6 +343,10 @@ namespace Emby.Server.Implementations.Session
_activeLiveStreamSessions.TryRemove(liveStreamId, out _);
}
}
else
{
liveStreamNeedsToBeClosed = true;
}
if (liveStreamNeedsToBeClosed)
{

View File

@@ -1,4 +1,5 @@
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
@@ -32,6 +33,8 @@ namespace Emby.Server.Implementations.Updates
/// </summary>
public class InstallationManager : IInstallationManager
{
private static readonly SearchValues<char> InvalidPackageNameChars = SearchValues.Create([.. Path.GetInvalidFileNameChars(), '/', '\\']);
/// <summary>
/// The logger.
/// </summary>
@@ -521,9 +524,27 @@ 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();
@@ -572,6 +593,26 @@ 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))