From 6ac2d707cba11d7986606bb8f2e00c03c7dc3258 Mon Sep 17 00:00:00 2001 From: Arty Date: Sat, 6 Sep 2025 00:38:30 -0400 Subject: Translated using Weblate (Russian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ru/ --- Emby.Server.Implementations/Localization/Core/ru.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index 84be91a872..1470a538c2 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -70,7 +70,7 @@ "ScheduledTaskFailedWithName": "{0} - неудачна", "ScheduledTaskStartedWithName": "{0} - запущена", "ServerNameNeedsToBeRestarted": "Необходим перезапуск {0}", - "Shows": "Телешоу", + "Shows": "Сериалы", "Songs": "Композиции", "StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.", "SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить", -- cgit v1.2.3 From 0845b0c258166b0793ed4f4abd2fb14e3efa85f4 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sun, 7 Sep 2025 07:02:52 -0400 Subject: Skip non-media folders in movie resolver (#14724) * Skip non-media folders in movie resolver * Ignorepatterns first --- .../Library/CoreResolutionIgnoreRule.cs | 10 +++++----- Emby.Server.Implementations/Library/IgnorePatterns.cs | 2 ++ .../Library/Resolvers/Movies/MovieResolver.cs | 5 +++++ 3 files changed, 12 insertions(+), 5 deletions(-) (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index f9538fbad6..ca0744a17d 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -37,6 +37,11 @@ namespace Emby.Server.Implementations.Library return false; } + if (IgnorePatterns.ShouldIgnore(fileInfo.FullName)) + { + return true; + } + // Don't ignore top level folders if (fileInfo.IsDirectory && (parent is AggregateFolder || (parent?.IsTopParent ?? false))) @@ -44,11 +49,6 @@ namespace Emby.Server.Implementations.Library return false; } - if (IgnorePatterns.ShouldIgnore(fileInfo.FullName)) - { - return true; - } - if (parent is null) { return false; diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index 25ddade829..fe3a1ce611 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -48,6 +48,8 @@ namespace Emby.Server.Implementations.Library "**/.wd_tv", "**/lost+found/**", "**/lost+found", + "**/subs/**", + "**/subs", // Trickplay files "**/*.trickplay", diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index b2ceee97d8..333c8c34bf 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -405,6 +405,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies if (child.IsDirectory) { + if (NamingOptions.AllExtrasTypesFolderNames.ContainsKey(filename)) + { + continue; + } + if (IsDvdDirectory(child.FullName, filename, directoryService)) { var movie = new T -- cgit v1.2.3 From aa3a7c88a4688fb7ce972dd8008f483a72728991 Mon Sep 17 00:00:00 2001 From: Magnus Antonsen Date: Mon, 8 Sep 2025 09:44:04 -0400 Subject: Translated using Weblate (Norwegian Bokmål) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nb_NO/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Emby.Server.Implementations/Localization/Core/nb.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index 8baa63d89f..e73c56cb90 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -136,5 +136,7 @@ "TaskExtractMediaSegments": "Skann mediasegment", "TaskMoveTrickplayImages": "Migrer bildeplassering for Trickplay", "TaskMoveTrickplayImagesDescription": "Flytter eksisterende Trickplay-filer i henhold til biblioteksinstillingene.", - "TaskExtractMediaSegmentsDescription": "Trekker ut eller henter mediasegmenter fra plugins som støtter MediaSegment." + "TaskExtractMediaSegmentsDescription": "Trekker ut eller henter mediasegmenter fra plugins som støtter MediaSegment.", + "CleanupUserDataTaskDescription": "Sletter all brukerdata (avspillings-status, favoritter osv.) fra innhold som har vært utilgjengelig i minst 90 dager.", + "CleanupUserDataTask": "Oppgave for opprydding av brukerdata" } -- cgit v1.2.3 From 1fa63b797b66959db6d0d84f45ebfee33c6d0656 Mon Sep 17 00:00:00 2001 From: Adrián HM Date: Mon, 8 Sep 2025 14:42:56 -0400 Subject: Translated using Weblate (Spanish (Mexico)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es_MX/ --- Emby.Server.Implementations/Localization/Core/es-MX.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json index 20f38de62f..52a26c1af2 100644 --- a/Emby.Server.Implementations/Localization/Core/es-MX.json +++ b/Emby.Server.Implementations/Localization/Core/es-MX.json @@ -137,5 +137,6 @@ "TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de plugins habilitados para MediaSegment.", "TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay", "TaskMoveTrickplayImagesDescription": "Mueve archivos de trickplay existentes según la configuración de la biblioteca.", - "CleanupUserDataTask": "Tarea de limpieza de los datos del usuario" + "CleanupUserDataTask": "Tarea de limpieza de los datos del usuario", + "CleanupUserDataTaskDescription": "Limpia toda la información de usuario (Estado de última vez visto, favoritos, etc) del archivo media que no está presente por los últimos 90 días." } -- cgit v1.2.3 From 387bc0c8eb9617636ff02e0c16d934f30596d6fb Mon Sep 17 00:00:00 2001 From: Looooke Date: Tue, 9 Sep 2025 16:45:55 -0400 Subject: Translated using Weblate (Alemannic) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/gsw/ --- Emby.Server.Implementations/Localization/Core/gsw.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/Localization/Core/gsw.json b/Emby.Server.Implementations/Localization/Core/gsw.json index b95d07d5cf..b3ee2a4f67 100644 --- a/Emby.Server.Implementations/Localization/Core/gsw.json +++ b/Emby.Server.Implementations/Localization/Core/gsw.json @@ -12,7 +12,7 @@ "DeviceOfflineWithName": "{0} wurde getrennt", "DeviceOnlineWithName": "{0} ist verbunden", "FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}", - "Favorites": "Favoriten", + "Favorites": "Favorite", "Folders": "Ordner", "Genres": "Genre", "HeaderAlbumArtists": "Album-Künstler", -- cgit v1.2.3 From 986a509955332e366e6fa53fb4b5b4f96f666e27 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Thu, 11 Sep 2025 17:24:23 -0400 Subject: Add 1-second tolerance to resume playback completion check (#14774) --- Emby.Server.Implementations/Library/UserDataManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index be1d96bf0b..b462064187 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -304,7 +304,7 @@ namespace Emby.Server.Implementations.Library // ignore progress during the beginning positionTicks = 0; } - else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= runtimeTicks) + else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= (runtimeTicks - TimeSpan.TicksPerSecond)) { // mark as completed close to the end positionTicks = 0; -- cgit v1.2.3 From bca6400bc34f3c6dd7e5bdddc20f026b814265cb Mon Sep 17 00:00:00 2001 From: nenadsuperzmaj Date: Fri, 12 Sep 2025 12:05:51 -0400 Subject: Translated using Weblate (Serbian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sr/ --- Emby.Server.Implementations/Localization/Core/sr.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json index af40b5e5a9..76a136cf56 100644 --- a/Emby.Server.Implementations/Localization/Core/sr.json +++ b/Emby.Server.Implementations/Localization/Core/sr.json @@ -126,5 +126,16 @@ "HearingImpaired": "ослабљен слух", "TaskAudioNormalization": "Нормализација звука", "TaskCleanCollectionsAndPlaylists": "Очистите колекције и плејлисте", - "TaskAudioNormalizationDescription": "Скенира датотеке за податке о нормализацији звука." + "TaskAudioNormalizationDescription": "Скенира датотеке за податке о нормализацији звука.", + "TaskRefreshTrickplayImages": "Направи сличице за визуелно премотавање", + "TaskRefreshTrickplayImagesDescription": "Прављење сличица које помажу код визуелног премотавања видео-снимака.", + "TaskDownloadMissingLyrics": "Преузми стихове који недостају", + "TaskCleanCollectionsAndPlaylistsDescription": "Уклања ставке које више не постоје из колекција и плејлиста.", + "TaskExtractMediaSegments": "Скенирај сегменте медија", + "TaskExtractMediaSegmentsDescription": "Извлачи или добавља сегменте медија у додацима који раде са MediaSegment-ом.", + "TaskMoveTrickplayImagesDescription": "Премешта постојеће сличице за визуелно премотавање сходно подешавањима библиотеке.", + "CleanupUserDataTask": "Задатак чишћења корисничких података", + "CleanupUserDataTaskDescription": "Чисти све корисничке податке (напредак гледања, ознаке за омиљено...) медија који нису доступни 90 дана или дуже.", + "TaskMoveTrickplayImages": "Промени локацију сличица за визуелно премотавање", + "TaskDownloadMissingLyricsDescription": "Преузми стихове песама" } -- cgit v1.2.3 From a99e67544a82da388ce041f836355f86dbc03609 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Fri, 12 Sep 2025 21:57:33 +0200 Subject: Reenable pooling (#14778) --- .../ScheduledTasks/Tasks/OptimizeDatabaseTask.cs | 2 +- .../SqliteDatabaseProvider.cs | 17 ++++++----------- 2 files changed, 7 insertions(+), 12 deletions(-) (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs index bf8ffaf479..92d7a3907a 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs @@ -61,7 +61,7 @@ public class OptimizeDatabaseTask : IScheduledTask, IConfigurableScheduledTask yield return new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, - IntervalTicks = TimeSpan.FromHours(24).Ticks + IntervalTicks = TimeSpan.FromHours(6).Ticks }; } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs index ccf84e6012..d51e8fd645 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs @@ -45,7 +45,7 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider var sqliteConnectionBuilder = new SqliteConnectionStringBuilder(); sqliteConnectionBuilder.DataSource = Path.Combine(_applicationPaths.DataPath, "jellyfin.db"); sqliteConnectionBuilder.Cache = Enum.Parse(databaseConfiguration.CustomProviderOptions?.Options.FirstOrDefault(e => e.Key.Equals("cache", StringComparison.OrdinalIgnoreCase))?.Value ?? nameof(SqliteCacheMode.Default)); - sqliteConnectionBuilder.Pooling = (databaseConfiguration.CustomProviderOptions?.Options.FirstOrDefault(e => e.Key.Equals("pooling", StringComparison.OrdinalIgnoreCase))?.Value ?? bool.FalseString).Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase); + sqliteConnectionBuilder.Pooling = (databaseConfiguration.CustomProviderOptions?.Options.FirstOrDefault(e => e.Key.Equals("pooling", StringComparison.OrdinalIgnoreCase))?.Value ?? bool.TrueString).Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase); var connectionString = sqliteConnectionBuilder.ToString(); @@ -74,16 +74,11 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using (context.ConfigureAwait(false)) { - if (context.Database.IsSqlite()) - { - await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false); - await context.Database.ExecuteSqlRawAsync("VACUUM", cancellationToken).ConfigureAwait(false); - _logger.LogInformation("jellyfin.db optimized successfully!"); - } - else - { - _logger.LogInformation("This database doesn't support optimization"); - } + await context.Database.ExecuteSqlRawAsync("PRAGMA wal_checkpoint(TRUNCATE)", cancellationToken).ConfigureAwait(false); + await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false); + await context.Database.ExecuteSqlRawAsync("VACUUM", cancellationToken).ConfigureAwait(false); + await context.Database.ExecuteSqlRawAsync("PRAGMA wal_checkpoint(TRUNCATE)", cancellationToken).ConfigureAwait(false); + _logger.LogInformation("jellyfin.db optimized successfully!"); } } -- cgit v1.2.3 From 8fcc2496d9f8eebc0e35a5dcb45d9eb28b8aad6c Mon Sep 17 00:00:00 2001 From: Alex Collado <57129654+a-collado@users.noreply.github.com> Date: Fri, 12 Sep 2025 19:57:48 +0000 Subject: Change Spanish order in iso6392.txt to favor Castillian (#14777) --- Emby.Server.Implementations/Localization/iso6392.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/Localization/iso6392.txt b/Emby.Server.Implementations/Localization/iso6392.txt index 5e65bae26f..d5a7e866b8 100644 --- a/Emby.Server.Implementations/Localization/iso6392.txt +++ b/Emby.Server.Implementations/Localization/iso6392.txt @@ -402,8 +402,8 @@ sog|||Sogdian|sogdien som||so|Somali|somali son|||Songhai languages|songhai, langues sot||st|Sotho, Southern|sotho du Sud -spa||es-419|Spanish; Latin|espagnol; Latin spa||es|Spanish; Castilian|espagnol; castillan +spa||es-419|Spanish; Latin|espagnol; Latin sqi|alb|sq|Albanian|albanais srd||sc|Sardinian|sarde srn|||Sranan Tongo|sranan tongo -- cgit v1.2.3 From deee04ae38cf057938f37c1dd7a98d20cf5b65f3 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Fri, 12 Sep 2025 21:58:02 +0200 Subject: Add fast path to check for empty ignore files (#14782) --- Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs index 401ca73b80..bafe3ad436 100644 --- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -50,6 +50,13 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule return false; } + // Fast path in case the ignore files isn't a symlink and is empty + if ((dirIgnoreFile.Attributes & FileAttributes.ReparsePoint) == 0 + && dirIgnoreFile.Length == 0) + { + return true; + } + // ignore the directory only if the .ignore file is empty // evaluate individual files otherwise return string.IsNullOrWhiteSpace(GetFileContent(dirIgnoreFile)); -- cgit v1.2.3 From 8776a447d1c2fc553d24bc1162f27017f84e80bb Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Fri, 12 Sep 2025 21:58:23 +0200 Subject: Various cleanups (#14785) --- Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs | 2 +- Emby.Server.Implementations/Library/MediaSourceManager.cs | 2 +- .../FullSystemBackup/BackupService.cs | 2 +- Jellyfin.Server/Filters/AdditionalModelFilter.cs | 2 +- Jellyfin.Server/Migrations/JellyfinMigrationService.cs | 6 +++--- Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs | 2 +- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 2 +- MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs | 8 +++----- MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs | 9 ++++++--- 9 files changed, 18 insertions(+), 17 deletions(-) (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index e74755ec32..c69bcfef78 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -108,7 +108,7 @@ namespace Emby.Server.Implementations.AppBase private void CheckOrCreateMarker(string path, string markerName, bool recursive = false) { var otherMarkers = GetMarkers(path, recursive).FirstOrDefault(e => Path.GetFileName(e) != markerName); - if (otherMarkers != null) + if (otherMarkers is not null) { throw new InvalidOperationException($"Exepected to find only {markerName} but found marker for {otherMarkers}."); } diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 1e3b8ea760..750346169f 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -657,7 +657,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception."); + _logger.LogDebug(ex, "Error parsing cached media info."); } finally { diff --git a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs index 74d99455df..e5c3cef3d3 100644 --- a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs +++ b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs @@ -39,7 +39,7 @@ public class BackupService : IBackupService ReferenceHandler = ReferenceHandler.IgnoreCycles, }; - private readonly Version _backupEngineVersion = Version.Parse("0.2.0"); + private readonly Version _backupEngineVersion = new Version(0, 2, 0); /// /// Initializes a new instance of the class. diff --git a/Jellyfin.Server/Filters/AdditionalModelFilter.cs b/Jellyfin.Server/Filters/AdditionalModelFilter.cs index 421eeecda0..58d37db5a5 100644 --- a/Jellyfin.Server/Filters/AdditionalModelFilter.cs +++ b/Jellyfin.Server/Filters/AdditionalModelFilter.cs @@ -175,7 +175,7 @@ namespace Jellyfin.Server.Filters // Manually generate sync play GroupUpdate messages. var groupUpdateTypes = typeof(GroupUpdate<>).Assembly.GetTypes() - .Where(t => t.BaseType != null + .Where(t => t.BaseType is not null && t.BaseType.IsGenericType && t.BaseType.GetGenericTypeDefinition() == typeof(GroupUpdate<>)) .ToList(); diff --git a/Jellyfin.Server/Migrations/JellyfinMigrationService.cs b/Jellyfin.Server/Migrations/JellyfinMigrationService.cs index fe191916c6..188d3c4a9a 100644 --- a/Jellyfin.Server/Migrations/JellyfinMigrationService.cs +++ b/Jellyfin.Server/Migrations/JellyfinMigrationService.cs @@ -62,7 +62,7 @@ internal class JellyfinMigrationService #pragma warning disable CS0618 // Type or member is obsolete Migrations = [.. typeof(IMigrationRoutine).Assembly.GetTypes().Where(e => typeof(IMigrationRoutine).IsAssignableFrom(e) || typeof(IAsyncMigrationRoutine).IsAssignableFrom(e)) .Select(e => (Type: e, Metadata: e.GetCustomAttribute(), Backup: e.GetCustomAttributes())) - .Where(e => e.Metadata != null) + .Where(e => e.Metadata is not null) .GroupBy(e => e.Metadata!.Stage) .Select(f => { @@ -137,7 +137,7 @@ internal class JellyfinMigrationService var migrationOptions = File.Exists(migrationConfigPath) ? (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)! : null; - if (migrationOptions != null && migrationOptions.Applied.Count > 0) + if (migrationOptions is not null && migrationOptions.Applied.Count > 0) { logger.LogInformation("Old migration style migration.xml detected. Migrate now."); try @@ -383,7 +383,7 @@ internal class JellyfinMigrationService } } - if (backupInstruction.JellyfinDb && _jellyfinDatabaseProvider != null) + if (backupInstruction.JellyfinDb && _jellyfinDatabaseProvider is not null) { logger.LogInformation("A migration will attempt to modify the jellyfin.db, will attempt to backup the file now."); _backupKey = (_backupKey.LibraryDb, await _jellyfinDatabaseProvider.MigrationBackupFast(CancellationToken.None).ConfigureAwait(false), _backupKey.FullBackup); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index e04a2737a6..e8ff00dd2f 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -1189,7 +1189,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine ItemId = baseItemId, Id = Guid.NewGuid(), Path = e.Path, - Blurhash = e.BlurHash != null ? Encoding.UTF8.GetBytes(e.BlurHash) : null, + Blurhash = e.BlurHash is not null ? Encoding.UTF8.GetBytes(e.BlurHash) : null, DateModified = e.DateModified, Height = e.Height, Width = e.Width, diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 92e0129409..72626e8532 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -98,7 +98,7 @@ public sealed class SetupServer : IDisposable var maxLevel = logEntry.LogLevel; var stack = new Stack(children); - while (maxLevel != LogLevel.Error && stack.Count > 0 && (logEntry = stack.Pop()) != null) // error is the highest inherted error level. + 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) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 3f94f54c3c..18646ec5dc 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -932,12 +932,10 @@ namespace MediaBrowser.MediaEncoding.Probing } var frameInfo = frameInfoList?.FirstOrDefault(i => i.StreamIndex == stream.Index); - if (frameInfo?.SideDataList != null) + if (frameInfo?.SideDataList is not null + && frameInfo.SideDataList.Any(data => string.Equals(data.SideDataType, "HDR Dynamic Metadata SMPTE2094-40 (HDR10+)", StringComparison.OrdinalIgnoreCase))) { - if (frameInfo.SideDataList.Any(data => string.Equals(data.SideDataType, "HDR Dynamic Metadata SMPTE2094-40 (HDR10+)", StringComparison.OrdinalIgnoreCase))) - { - stream.Hdr10PlusPresentFlag = true; - } + stream.Hdr10PlusPresentFlag = true; } } else if (streamInfo.CodecType == CodecType.Data) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index 8bef9bd74e..ab072be03f 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -213,15 +213,18 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies var releases = movieResult.Releases.Countries.Where(i => !string.IsNullOrWhiteSpace(i.Certification)).ToList(); var ourRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, info.MetadataCountryCode, StringComparison.OrdinalIgnoreCase)); - var usRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase)); if (ourRelease is not null) { movie.OfficialRating = TmdbUtils.BuildParentalRating(ourRelease.Iso_3166_1, ourRelease.Certification); } - else if (usRelease is not null) + else { - movie.OfficialRating = usRelease.Certification; + var usRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase)); + if (usRelease is not null) + { + movie.OfficialRating = usRelease.Certification; + } } } -- cgit v1.2.3 From 4d36bd635d3dd0ff5652c1807dce7a1a1dff8873 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Sun, 14 Sep 2025 11:18:21 -0600 Subject: Revert IsPlayed optimization, pass UserItemData to IsPlayed when available (#14786) --- .../Sorting/IsFavoriteOrLikeComparer.cs | 3 +-- .../Sorting/IsPlayedComparer.cs | 3 +-- .../Sorting/IsUnplayedComparer.cs | 3 +-- MediaBrowser.Controller/Entities/BaseItem.cs | 18 +++++++++--------- MediaBrowser.Controller/Entities/Folder.cs | 8 ++++---- MediaBrowser.Controller/Entities/UserViewBuilder.cs | 2 +- 6 files changed, 17 insertions(+), 20 deletions(-) (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs index 01c1e596f9..86d08ed27b 100644 --- a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs @@ -6,7 +6,6 @@ using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; -using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { @@ -54,7 +53,7 @@ namespace Emby.Server.Implementations.Sorting /// DateTime. private int GetValue(BaseItem x) { - return x.IsFavoriteOrLiked(User) ? 0 : 1; + return x.IsFavoriteOrLiked(User, userItemData: null) ? 0 : 1; } } } diff --git a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs index 6f206c8772..9faa02f1fd 100644 --- a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs @@ -7,7 +7,6 @@ using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; -using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { @@ -55,7 +54,7 @@ namespace Emby.Server.Implementations.Sorting /// DateTime. private int GetValue(BaseItem x) { - return x.IsPlayed(User) ? 0 : 1; + return x.IsPlayed(User, userItemData: null) ? 0 : 1; } } } diff --git a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs index fd1326327b..6f177c4637 100644 --- a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs @@ -7,7 +7,6 @@ using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; -using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { @@ -55,7 +54,7 @@ namespace Emby.Server.Implementations.Sorting /// DateTime. private int GetValue(BaseItem x) { - return x.IsUnplayed(User) ? 0 : 1; + return x.IsUnplayed(User, userItemData: null) ? 0 : 1; } } } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 275fdac2eb..67675e7560 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -2315,27 +2315,27 @@ namespace MediaBrowser.Controller.Entities return UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None); } - public virtual bool IsPlayed(User user) + public virtual bool IsPlayed(User user, UserItemData userItemData) { - var userdata = UserDataManager.GetUserData(user, this); + userItemData ??= UserDataManager.GetUserData(user, this); - return userdata is not null && userdata.Played; + return userItemData is not null && userItemData.Played; } - public bool IsFavoriteOrLiked(User user) + public bool IsFavoriteOrLiked(User user, UserItemData userItemData) { - var userdata = UserDataManager.GetUserData(user, this); + userItemData ??= UserDataManager.GetUserData(user, this); - return userdata is not null && (userdata.IsFavorite || (userdata.Likes ?? false)); + return userItemData is not null && (userItemData.IsFavorite || (userItemData.Likes ?? false)); } - public virtual bool IsUnplayed(User user) + public virtual bool IsUnplayed(User user, UserItemData userItemData) { ArgumentNullException.ThrowIfNull(user); - var userdata = UserDataManager.GetUserData(user, this); + userItemData ??= UserDataManager.GetUserData(user, this); - return userdata is null || !userdata.Played; + return userItemData is null || !userItemData.Played; } ItemLookupInfo IHasLookupInfo.GetLookupInfo() diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 082cf39fac..b889e73e3a 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1666,7 +1666,7 @@ namespace MediaBrowser.Controller.Entities } } - public override bool IsPlayed(User user) + public override bool IsPlayed(User user, UserItemData userItemData) { var itemsResult = GetItemList(new InternalItemsQuery(user) { @@ -1677,12 +1677,12 @@ namespace MediaBrowser.Controller.Entities }); return itemsResult - .All(i => i.IsPlayed(user)); + .All(i => i.IsPlayed(user, userItemData: null)); } - public override bool IsUnplayed(User user) + public override bool IsUnplayed(User user, UserItemData userItemData) { - return !IsPlayed(user); + return !IsPlayed(user, userItemData); } public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields) diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 0cd3399d4a..62eb43aa52 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -542,7 +542,7 @@ namespace MediaBrowser.Controller.Entities if (query.IsPlayed.HasValue) { userData ??= userDataManager.GetUserData(user, item); - if (userData.Played != query.IsPlayed.Value) + if (item.IsPlayed(user, userData) != query.IsPlayed.Value) { return false; } -- cgit v1.2.3 From a0b3e2b071509f440db10768f6f8984c7ea382d6 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Tue, 16 Sep 2025 21:08:04 +0200 Subject: Optimize internal querying of UserData, other fixes (#14795) --- .../Library/LibraryManager.cs | 10 +- .../Library/MusicManager.cs | 13 +- .../Library/UserDataManager.cs | 8 +- Jellyfin.Api/Controllers/YearsController.cs | 5 +- .../Item/BaseItemRepository.cs | 104 +- .../Item/PeopleRepository.cs | 6 +- MediaBrowser.Controller/Entities/BaseItem.cs | 7 + MediaBrowser.Controller/Entities/Folder.cs | 122 +- MediaBrowser.Controller/Entities/Movies/BoxSet.cs | 4 +- MediaBrowser.Controller/Entities/TV/Season.cs | 2 +- MediaBrowser.Controller/Entities/TV/Series.cs | 1 + MediaBrowser.Controller/Entities/UserView.cs | 10 +- .../Persistence/IItemRepository.cs | 10 + MediaBrowser.Controller/Playlists/Playlist.cs | 6 +- .../Entities/BaseItemEntity.cs | 4 + .../ModelConfiguration/BaseItemConfiguration.cs | 1 + ...entChildRelationBaseItemWithCascade.Designer.cs | 1721 ++++++++++++++++++++ ...ProperParentChildRelationBaseItemWithCascade.cs | 30 + .../Migrations/JellyfinDbModelSnapshot.cs | 14 +- .../Controllers/LibraryStructureControllerTests.cs | 2 + 20 files changed, 1988 insertions(+), 92 deletions(-) create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.Designer.cs create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.cs (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 58a971f62a..0074df80a0 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1090,6 +1090,7 @@ namespace Emby.Server.Implementations.Library public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false) { + RootFolder.Children = null; await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); // Start by just validating the children of the root, but go no further @@ -1100,9 +1101,12 @@ namespace Emby.Server.Implementations.Library allowRemoveRoot: removeRoot, cancellationToken: cancellationToken).ConfigureAwait(false); - await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false); + var rootFolder = GetUserRootFolder(); + rootFolder.Children = null; - await GetUserRootFolder().ValidateChildren( + await rootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); + + await rootFolder.ValidateChildren( new Progress(), new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: false, @@ -1110,7 +1114,7 @@ namespace Emby.Server.Implementations.Library cancellationToken: cancellationToken).ConfigureAwait(false); // Quickly scan CollectionFolders for changes - foreach (var child in GetUserRootFolder().Children.OfType()) + foreach (var child in rootFolder.Children!.OfType()) { // If the user has somehow deleted the collection directory, remove the metadata from the database. if (child is CollectionFolder collectionFolder && !Directory.Exists(collectionFolder.Path)) diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index 28cf695007..e0c8ae371b 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -45,11 +45,14 @@ namespace Emby.Server.Implementations.Library public IReadOnlyList GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions) { var genres = item - .GetRecursiveChildren(user, new InternalItemsQuery(user) - { - IncludeItemTypes = [BaseItemKind.Audio], - DtoOptions = dtoOptions - }) + .GetRecursiveChildren( + user, + new InternalItemsQuery(user) + { + IncludeItemTypes = [BaseItemKind.Audio], + DtoOptions = dtoOptions + }, + out _) .Cast public class Folder : BaseItem { + private IEnumerable _children; + public Folder() { LinkedChildren = Array.Empty(); @@ -108,11 +110,15 @@ namespace MediaBrowser.Controller.Entities } /// - /// Gets the actual children. + /// Gets or Sets the actual children. /// /// The actual children. [JsonIgnore] - public virtual IEnumerable Children => LoadChildren(); + public virtual IEnumerable Children + { + get => _children ??= LoadChildren(); + set => _children = value; + } /// /// Gets thread-safe access to all recursive children of this folder - without regard to user. @@ -281,6 +287,7 @@ namespace MediaBrowser.Controller.Entities /// Task. public Task ValidateChildren(IProgress progress, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true, bool allowRemoveRoot = false, CancellationToken cancellationToken = default) { + Children = null; // invalidate cached children. return ValidateChildrenInternal(progress, recursive, true, allowRemoveRoot, metadataRefreshOptions, metadataRefreshOptions.DirectoryService, cancellationToken); } @@ -288,6 +295,7 @@ namespace MediaBrowser.Controller.Entities { var dictionary = new Dictionary(); + Children = null; // invalidate cached children. var childrenList = Children.ToList(); foreach (var child in childrenList) @@ -526,6 +534,7 @@ namespace MediaBrowser.Controller.Entities { if (validChildrenNeedGeneration) { + Children = null; // invalidate cached children. validChildren = Children.ToList(); } @@ -568,6 +577,7 @@ namespace MediaBrowser.Controller.Entities if (recursive && child is Folder folder) { + folder.Children = null; // invalidate cached children. await folder.RefreshMetadataRecursive(folder.Children.Except([this, child]).ToList(), refreshOptions, true, progress, cancellationToken).ConfigureAwait(false); } } @@ -686,16 +696,22 @@ namespace MediaBrowser.Controller.Entities IEnumerable items; Func filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager); + var totalCount = 0; if (query.User is null) { items = GetRecursiveChildren(filter); + totalCount = items.Count(); } else { - items = GetRecursiveChildren(user, query); + items = GetRecursiveChildren(user, query, out totalCount); + query.Limit = null; + query.StartIndex = null; // override these here as they have already been applied } - return PostFilterAndSort(items, query); + var result = PostFilterAndSort(items, query); + result.TotalRecordCount = totalCount; + return result; } if (this is not UserRootFolder @@ -944,22 +960,31 @@ namespace MediaBrowser.Controller.Entities IEnumerable items; + int totalItemCount = 0; if (query.User is null) { items = Children.Where(filter); + totalItemCount = items.Count(); } else { // need to pass this param to the children. var childQuery = new InternalItemsQuery { - DisplayAlbumFolders = query.DisplayAlbumFolders + DisplayAlbumFolders = query.DisplayAlbumFolders, + Limit = query.Limit, + StartIndex = query.StartIndex }; - items = GetChildren(user, true, childQuery).Where(filter); + items = GetChildren(user, true, out totalItemCount, childQuery).Where(filter); + + query.Limit = null; + query.StartIndex = null; } - return PostFilterAndSort(items, query); + var result = PostFilterAndSort(items, query); + result.TotalRecordCount = totalItemCount; + return result; } protected QueryResult PostFilterAndSort(IEnumerable items, InternalItemsQuery query) @@ -1242,30 +1267,30 @@ namespace MediaBrowser.Controller.Entities return true; } - public IReadOnlyList GetChildren(User user, bool includeLinkedChildren) - { - ArgumentNullException.ThrowIfNull(user); - - return GetChildren(user, includeLinkedChildren, new InternalItemsQuery(user)); - } - - public virtual IReadOnlyList GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + public virtual IReadOnlyList GetChildren(User user, bool includeLinkedChildren, out int totalItemCount, InternalItemsQuery query = null) { ArgumentNullException.ThrowIfNull(user); + query ??= new InternalItemsQuery(); + query.User = user; // the true root should return our users root folder children if (IsPhysicalRoot) { - return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren); + return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren, out totalItemCount); } var result = new Dictionary(); - AddChildren(user, includeLinkedChildren, result, false, query); + totalItemCount = AddChildren(user, includeLinkedChildren, result, false, query); return result.Values.ToArray(); } + public virtual IReadOnlyList GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query = null) + { + return GetChildren(user, includeLinkedChildren, out _, query); + } + protected virtual IEnumerable GetEligibleChildrenForRecursiveChildren(User user) { return Children; @@ -1274,13 +1299,13 @@ namespace MediaBrowser.Controller.Entities /// /// Adds the children to list. /// - private void AddChildren(User user, bool includeLinkedChildren, Dictionary result, bool recursive, InternalItemsQuery query, HashSet visitedFolders = null) + private int AddChildren(User user, bool includeLinkedChildren, Dictionary result, bool recursive, InternalItemsQuery query, HashSet visitedFolders = null) { // Prevent infinite recursion of nested folders visitedFolders ??= new HashSet(); if (!visitedFolders.Add(this)) { - return; + return 0; } // If Query.AlbumFolders is set, then enforce the format as per the db in that it permits sub-folders in music albums. @@ -1297,44 +1322,58 @@ namespace MediaBrowser.Controller.Entities children = GetEligibleChildrenForRecursiveChildren(user); } - AddChildrenFromCollection(children, user, includeLinkedChildren, result, recursive, query, visitedFolders); - if (includeLinkedChildren) { - AddChildrenFromCollection(GetLinkedChildren(user), user, includeLinkedChildren, result, recursive, query, visitedFolders); + children = children.Concat(GetLinkedChildren(user)).ToArray(); } + + return AddChildrenFromCollection(children, user, includeLinkedChildren, result, recursive, query, visitedFolders); } - private void AddChildrenFromCollection(IEnumerable children, User user, bool includeLinkedChildren, Dictionary result, bool recursive, InternalItemsQuery query, HashSet visitedFolders) + private int AddChildrenFromCollection(IEnumerable children, User user, bool includeLinkedChildren, Dictionary result, bool recursive, InternalItemsQuery query, HashSet visitedFolders) { - foreach (var child in children) - { - if (!child.IsVisible(user)) - { - continue; - } + query ??= new InternalItemsQuery(); + var limit = query.Limit; + query.Limit = 100; // this is a bit of a dirty hack thats in favor of specifically the webUI as it does not show more then +99 elements in its badges so there is no point in reading more then that. + + var visibileChildren = children + .Where(e => e.IsVisible(user)) + .ToArray(); - if (query is null || UserViewBuilder.FilterItem(child, query)) + var realChildren = visibileChildren + .Where(e => query is null || UserViewBuilder.FilterItem(e, query)) + .ToArray(); + var childCount = realChildren.Count(); + if (result.Count < query.Limit) + { + foreach (var child in realChildren + .Skip(query.StartIndex ?? 0) + .TakeWhile(e => query.Limit >= result.Count)) { result[child.Id] = child; } + } - if (recursive && child.IsFolder) + if (recursive) + { + foreach (var child in visibileChildren + .Where(e => e.IsFolder) + .OfType()) { - var folder = (Folder)child; - - folder.AddChildren(user, includeLinkedChildren, result, true, query, visitedFolders); + childCount += child.AddChildren(user, includeLinkedChildren, result, true, query, visitedFolders); } } + + return childCount; } - public virtual IReadOnlyList GetRecursiveChildren(User user, InternalItemsQuery query) + public virtual IReadOnlyList GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount) { ArgumentNullException.ThrowIfNull(user); var result = new Dictionary(); - AddChildren(user, true, result, true, query); + totalCount = AddChildren(user, true, result, true, query); return result.Values.ToArray(); } @@ -1668,16 +1707,7 @@ namespace MediaBrowser.Controller.Entities public override bool IsPlayed(User user, UserItemData userItemData) { - var itemsResult = GetItemList(new InternalItemsQuery(user) - { - Recursive = true, - IsFolder = false, - IsVirtualItem = false, - EnableTotalRecordCount = false - }); - - return itemsResult - .All(i => i.IsPlayed(user, userItemData: null)); + return ItemRepository.GetIsPlayed(user, Id, true); } public override bool IsUnplayed(User user, UserItemData userItemData) diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index dd5852823e..1d1fb2c392 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -136,9 +136,9 @@ namespace MediaBrowser.Controller.Entities.Movies return Sort(children, user).ToArray(); } - public override IReadOnlyList GetRecursiveChildren(User user, InternalItemsQuery query) + public override IReadOnlyList GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount) { - var children = base.GetRecursiveChildren(user, query); + var children = base.GetRecursiveChildren(user, query, out totalCount); return Sort(children, user).ToArray(); } diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 48211d99f2..b972ebaa6b 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -123,7 +123,7 @@ namespace MediaBrowser.Controller.Entities.TV public override int GetChildCount(User user) { - var result = GetChildren(user, true).Count; + var result = GetChildren(user, true, null).Count; return result; } diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 62c73d56f8..427c2995bc 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -297,6 +297,7 @@ namespace MediaBrowser.Controller.Entities.TV public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress progress, CancellationToken cancellationToken) { + Children = null; // invalidate cached children. // Refresh bottom up, seasons and episodes first, then the series var items = GetRecursiveChildren(); diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index dfa31315cb..5624f8b2e9 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -89,7 +89,7 @@ namespace MediaBrowser.Controller.Entities /// public override int GetChildCount(User user) { - return GetChildren(user, true).Count; + return GetChildren(user, true, null).Count; } /// @@ -134,20 +134,22 @@ namespace MediaBrowser.Controller.Entities } /// - public override IReadOnlyList GetRecursiveChildren(User user, InternalItemsQuery query) + public override IReadOnlyList GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount) { query.SetUser(user); query.Recursive = true; query.EnableTotalRecordCount = false; query.ForceDirect = true; + var data = GetItemList(query); + totalCount = data.Count; - return GetItemList(query); + return data; } /// protected override IReadOnlyList GetEligibleChildrenForRecursiveChildren(User user) { - return GetChildren(user, false); + return GetChildren(user, false, null); } public static bool IsUserSpecific(Folder folder) diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index a0dabbac62..e17dc38f7f 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; @@ -112,4 +113,13 @@ public interface IItemRepository /// The id to check. /// True if the item exists, otherwise false. Task ItemExistsAsync(Guid id); + + /// + /// Gets a value indicating wherever all children of the requested Id has been played. + /// + /// The userdata to check against. + /// The Top id to check. + /// Whever the check should be done recursive. Warning expensive operation. + /// A value indicating whever all children has been played. + bool GetIsPlayed(User user, Guid id, bool recursive); } diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index 1062399e3f..fc367b8293 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -149,9 +149,11 @@ namespace MediaBrowser.Controller.Playlists return []; } - public override IReadOnlyList GetRecursiveChildren(User user, InternalItemsQuery query) + public override IReadOnlyList GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount) { - return GetPlayableItems(user, query); + var items = GetPlayableItems(user, query); + totalCount = items.Count; + return items; } public IReadOnlyList> GetManageableItems() diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs index a09a96317c..d58466e5ca 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs @@ -146,6 +146,8 @@ public class BaseItemEntity public Guid? ParentId { get; set; } + public BaseItemEntity? DirectParent { get; set; } + public Guid? TopParentId { get; set; } public Guid? SeasonId { get; set; } @@ -168,6 +170,8 @@ public class BaseItemEntity public ICollection? Children { get; set; } + public ICollection? DirectChildren { get; set; } + public ICollection? LockedFields { get; set; } public ICollection? TrailerTypes { get; set; } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs index bcf458abd5..6fccfd976d 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs @@ -27,6 +27,7 @@ public class BaseItemConfiguration : IEntityTypeConfiguration builder.HasMany(e => e.Provider); builder.HasMany(e => e.Parents); builder.HasMany(e => e.Children); + builder.HasMany(e => e.DirectChildren).WithOne(e => e.DirectParent).HasForeignKey(e => e.ParentId).OnDelete(DeleteBehavior.Cascade); builder.HasMany(e => e.LockedFields); builder.HasMany(e => e.TrailerTypes); builder.HasMany(e => e.Images); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.Designer.cs new file mode 100644 index 0000000000..5c5464a46c --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.Designer.cs @@ -0,0 +1,1721 @@ +// +using System; +using Jellyfin.Database.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDbContext))] + [Migration("20250913211637_AddProperParentChildRelationBaseItemWithCascade")] + partial class AddProperParentChildRelationBaseItemWithCascade + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.9"); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ParentItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ParentItemId"); + + b.HasIndex("ParentItemId"); + + b.ToTable("AncestorIds"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Filename") + .HasColumnType("TEXT"); + + b.Property("MimeType") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "Index"); + + b.ToTable("AttachmentStreamInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("AlbumArtists") + .HasColumnType("TEXT"); + + b.Property("Artists") + .HasColumnType("TEXT"); + + b.Property("Audio") + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("TEXT"); + + b.Property("CleanName") + .HasColumnType("TEXT"); + + b.Property("CommunityRating") + .HasColumnType("REAL"); + + b.Property("CriticRating") + .HasColumnType("REAL"); + + b.Property("CustomRating") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastMediaAdded") + .HasColumnType("TEXT"); + + b.Property("DateLastRefreshed") + .HasColumnType("TEXT"); + + b.Property("DateLastSaved") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("EpisodeTitle") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasColumnType("TEXT"); + + b.Property("ExternalSeriesId") + .HasColumnType("TEXT"); + + b.Property("ExternalServiceId") + .HasColumnType("TEXT"); + + b.Property("ExtraIds") + .HasColumnType("TEXT"); + + b.Property("ExtraType") + .HasColumnType("INTEGER"); + + b.Property("ForcedSortName") + .HasColumnType("TEXT"); + + b.Property("Genres") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IndexNumber") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingSubValue") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingValue") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("IsInMixedFolder") + .HasColumnType("INTEGER"); + + b.Property("IsLocked") + .HasColumnType("INTEGER"); + + b.Property("IsMovie") + .HasColumnType("INTEGER"); + + b.Property("IsRepeat") + .HasColumnType("INTEGER"); + + b.Property("IsSeries") + .HasColumnType("INTEGER"); + + b.Property("IsVirtualItem") + .HasColumnType("INTEGER"); + + b.Property("LUFS") + .HasColumnType("REAL"); + + b.Property("MediaType") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizationGain") + .HasColumnType("REAL"); + + b.Property("OfficialRating") + .HasColumnType("TEXT"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ParentIndexNumber") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataCountryCode") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataLanguage") + .HasColumnType("TEXT"); + + b.Property("PremiereDate") + .HasColumnType("TEXT"); + + b.Property("PresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("PrimaryVersionId") + .HasColumnType("TEXT"); + + b.Property("ProductionLocations") + .HasColumnType("TEXT"); + + b.Property("ProductionYear") + .HasColumnType("INTEGER"); + + b.Property("RunTimeTicks") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("TEXT"); + + b.Property("SeasonName") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("SeriesName") + .HasColumnType("TEXT"); + + b.Property("SeriesPresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("ShowId") + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Studios") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("TopParentId") + .HasColumnType("TEXT"); + + b.Property("TotalBitrate") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UnratedType") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("Path"); + + b.HasIndex("PresentationUniqueKey"); + + b.HasIndex("TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "PresentationUniqueKey"); + + b.HasIndex("Type", "TopParentId", "StartDate"); + + b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem"); + + b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName"); + + b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.ToTable("BaseItems"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0000-000000000001"), + IsFolder = false, + IsInMixedFolder = false, + IsLocked = false, + IsMovie = false, + IsRepeat = false, + IsSeries = false, + IsVirtualItem = false, + Name = "This is a placeholder item for UserData that has been detacted from its original item", + Type = "PLACEHOLDER" + }); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Blurhash") + .HasColumnType("BLOB"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("ImageType") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemImageInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemMetadataFields"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ProviderId"); + + b.HasIndex("ProviderId", "ProviderValue", "ItemId"); + + b.ToTable("BaseItemProviders"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemTrailerTypes"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ChapterIndex") + .HasColumnType("INTEGER"); + + b.Property("ImageDateModified") + .HasColumnType("TEXT"); + + b.Property("ImagePath") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("StartPositionTicks") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "ChapterIndex"); + + b.ToTable("Chapters"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b => + { + b.Property("ItemValueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CleanValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId"); + + b.HasIndex("Type", "CleanValue"); + + b.HasIndex("Type", "Value") + .IsUnique(); + + b.ToTable("ItemValues"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b => + { + b.Property("ItemValueId") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("ItemValuesMap"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.PrimitiveCollection("KeyframeTicks") + .HasColumnType("TEXT"); + + b.Property("TotalDuration") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId"); + + b.ToTable("KeyframeData"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EndTicks") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("SegmentProviderId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartTicks") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSegments"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("AspectRatio") + .HasColumnType("TEXT"); + + b.Property("AverageFrameRate") + .HasColumnType("REAL"); + + b.Property("BitDepth") + .HasColumnType("INTEGER"); + + b.Property("BitRate") + .HasColumnType("INTEGER"); + + b.Property("BlPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("ChannelLayout") + .HasColumnType("TEXT"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("CodecTimeBase") + .HasColumnType("TEXT"); + + b.Property("ColorPrimaries") + .HasColumnType("TEXT"); + + b.Property("ColorSpace") + .HasColumnType("TEXT"); + + b.Property("ColorTransfer") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("DvBlSignalCompatibilityId") + .HasColumnType("INTEGER"); + + b.Property("DvLevel") + .HasColumnType("INTEGER"); + + b.Property("DvProfile") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMajor") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMinor") + .HasColumnType("INTEGER"); + + b.Property("ElPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("Hdr10PlusPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IsAnamorphic") + .HasColumnType("INTEGER"); + + b.Property("IsAvc") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsExternal") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("IsHearingImpaired") + .HasColumnType("INTEGER"); + + b.Property("IsInterlaced") + .HasColumnType("INTEGER"); + + b.Property("KeyFrames") + .HasColumnType("TEXT"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("REAL"); + + b.Property("NalLengthSize") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PixelFormat") + .HasColumnType("TEXT"); + + b.Property("Profile") + .HasColumnType("TEXT"); + + b.Property("RealFrameRate") + .HasColumnType("REAL"); + + b.Property("RefFrames") + .HasColumnType("INTEGER"); + + b.Property("Rotation") + .HasColumnType("INTEGER"); + + b.Property("RpuPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("SampleRate") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .HasColumnType("INTEGER"); + + b.Property("TimeBase") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "StreamIndex"); + + b.HasIndex("StreamIndex"); + + b.HasIndex("StreamType"); + + b.HasIndex("StreamIndex", "StreamType"); + + b.HasIndex("StreamIndex", "StreamType", "Language"); + + b.ToTable("MediaStreamInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PersonType") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Peoples"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("PeopleId") + .HasColumnType("TEXT"); + + b.Property("ListOrder") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.HasIndex("ItemId", "ListOrder"); + + b.HasIndex("ItemId", "SortOrder"); + + b.ToTable("PeopleBaseItemMap"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.DeviceOptions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalRatingScore") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalRatingSubScore") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("CustomDataKey") + .HasColumnType("TEXT"); + + b.Property("AudioStreamIndex") + .HasColumnType("INTEGER"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER"); + + b.Property("LastPlayedDate") + .HasColumnType("TEXT"); + + b.Property("Likes") + .HasColumnType("INTEGER"); + + b.Property("PlayCount") + .HasColumnType("INTEGER"); + + b.Property("PlaybackPositionTicks") + .HasColumnType("INTEGER"); + + b.Property("Played") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("RetentionDate") + .HasColumnType("TEXT"); + + b.Property("SubtitleStreamIndex") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "UserId", "CustomDataKey"); + + b.HasIndex("UserId"); + + b.HasIndex("ItemId", "UserId", "IsFavorite"); + + b.HasIndex("ItemId", "UserId", "LastPlayedDate"); + + b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks"); + + b.HasIndex("ItemId", "UserId", "Played"); + + b.ToTable("UserData"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Parents") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem") + .WithMany("Children") + .HasForeignKey("ParentItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ParentItem"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "DirectParent") + .WithMany("DirectChildren") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("DirectParent"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Images") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("LockedFields") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Provider") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("TrailerTypes") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Chapters") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("ItemValues") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue") + .WithMany("BaseItemsMap") + .HasForeignKey("ItemValueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ItemValue"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("MediaStreams") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Peoples") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People") + .WithMany("BaseItems") + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("People"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("UserData") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => + { + b.Navigation("Chapters"); + + b.Navigation("Children"); + + b.Navigation("DirectChildren"); + + b.Navigation("Images"); + + b.Navigation("ItemValues"); + + b.Navigation("LockedFields"); + + b.Navigation("MediaStreams"); + + b.Navigation("Parents"); + + b.Navigation("Peoples"); + + b.Navigation("Provider"); + + b.Navigation("TrailerTypes"); + + b.Navigation("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b => + { + b.Navigation("BaseItemsMap"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b => + { + b.Navigation("BaseItems"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.cs new file mode 100644 index 0000000000..77f41edad2 --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// + public partial class AddProperParentChildRelationBaseItemWithCascade : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddForeignKey( + name: "FK_BaseItems_BaseItems_ParentId", + table: "BaseItems", + column: "ParentId", + principalTable: "BaseItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_BaseItems_BaseItems_ParentId", + table: "BaseItems"); + } + } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs index a7ff802afd..782f979f29 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs @@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.7"); + modelBuilder.HasAnnotation("ProductVersion", "9.0.9"); modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b => { @@ -1450,6 +1450,16 @@ namespace Jellyfin.Server.Implementations.Migrations b.Navigation("Item"); }); + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "DirectParent") + .WithMany("DirectChildren") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("DirectParent"); + }); + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b => { b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") @@ -1652,6 +1662,8 @@ namespace Jellyfin.Server.Implementations.Migrations b.Navigation("Children"); + b.Navigation("DirectChildren"); + b.Navigation("Images"); b.Navigation("ItemValues"); diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs index e7166d4246..36f1b726da 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs @@ -79,6 +79,8 @@ public sealed class LibraryStructureControllerTests : IClassFixture Date: Thu, 18 Sep 2025 14:37:31 +0000 Subject: Fix playlist move from smaller to larger index (#14794) --- Emby.Server.Implementations/Playlists/PlaylistManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 1ce363de5c..c9d76df0bf 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -314,7 +314,7 @@ namespace Emby.Server.Implementations.Playlists return; } - var newPriorItemIndex = newIndex > oldIndexAccessible ? newIndex : newIndex - 1 < 0 ? 0 : newIndex - 1; + var newPriorItemIndex = Math.Max(newIndex - 1, 0); var newPriorItemId = accessibleChildren[newPriorItemIndex].Item1.ItemId; var newPriorItemIndexOnAllChildren = children.FindIndex(c => c.Item1.ItemId.Equals(newPriorItemId)); var adjustedNewIndex = DetermineAdjustedIndex(newPriorItemIndexOnAllChildren, newIndex); -- cgit v1.2.3 From a1b85a63e7e34243916e27df90769d260b1c84df Mon Sep 17 00:00:00 2001 From: JPVenson Date: Fri, 19 Sep 2025 19:47:41 +0200 Subject: Fix root folder not being saved to Db if nessesary (#14819) * Fix root folder not being saved to Db if nessesary * Always update folder to Db --- Emby.Server.Implementations/Library/LibraryManager.cs | 1 + 1 file changed, 1 insertion(+) (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 0074df80a0..2e4d1b4c96 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -826,6 +826,7 @@ namespace Emby.Server.Implementations.Library if (!folder.ParentId.Equals(rootFolder.Id)) { + rootFolder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResult(); folder.ParentId = rootFolder.Id; folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResult(); } -- cgit v1.2.3 From b73ea1b99d799deb7cb8a530cc01b98b403ea519 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sat, 20 Sep 2025 15:20:21 +0200 Subject: Skip removed images (#14823) --- Emby.Server.Implementations/Library/LibraryManager.cs | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 2e4d1b4c96..a66835dec0 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2032,6 +2032,12 @@ namespace Emby.Server.Implementations.Library } } + if (!File.Exists(image.Path)) + { + _logger.LogWarning("Image not found at {ImagePath}", image.Path); + continue; + } + ImageDimensions size; try { -- cgit v1.2.3 From 717e7cbd77c51b624d6152aed3165c5be49dc8d2 Mon Sep 17 00:00:00 2001 From: Janniry Belen Date: Sun, 21 Sep 2025 22:43:05 -0400 Subject: Translated using Weblate (Spanish (Dominican Republic)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es_DO/ --- Emby.Server.Implementations/Localization/Core/es_DO.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/Localization/Core/es_DO.json b/Emby.Server.Implementations/Localization/Core/es_DO.json index 8cdd06b7c4..f98a5e5b2c 100644 --- a/Emby.Server.Implementations/Localization/Core/es_DO.json +++ b/Emby.Server.Implementations/Localization/Core/es_DO.json @@ -125,5 +125,11 @@ "Undefined": "Sin definir", "TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.", "TaskCleanCacheDescription": "Elimina archivos caché que ya no son necesarios para el sistema.", - "TaskCleanLogsDescription": "Elimina archivos de registro con más de {0} días de antigüedad." + "TaskCleanLogsDescription": "Elimina archivos de registro con más de {0} días de antigüedad.", + "NotificationOptionApplicationUpdateAvailable": "actualización disponible", + "TaskDownloadMissingLyrics": "Descargue letras desaparecidas", + "TaskDownloadMissingLyricsDescription": "Decarga letras para canciones", + "TaskMoveTrickplayImages": "Mover localización de foto vista previa", + "NotificationOptionApplicationUpdateInstalled": "Aplicación actualización disponible", + "CleanupUserDataTask": "Tarea de limpieza de los datos del usuario" } -- cgit v1.2.3 From 0d2c551cce745e50266426e96ee00c5282de43bd Mon Sep 17 00:00:00 2001 From: Jan Zachar Date: Mon, 22 Sep 2025 07:21:14 -0400 Subject: Translated using Weblate (Belarusian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/be/ --- .../Localization/Core/be.json | 74 +++++++++++----------- 1 file changed, 37 insertions(+), 37 deletions(-) (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json index dec491d08b..29847048cb 100644 --- a/Emby.Server.Implementations/Localization/Core/be.json +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -1,16 +1,16 @@ { "Sync": "Сінхранізаваць", - "Playlists": "Спісы прайгравання", - "Latest": "Апошні", + "Playlists": "Плэй-лісты", + "Latest": "Апошняе", "LabelIpAddressValue": "IP-адрас: {0}", - "ItemAddedWithName": "{0} быў дададзены ў бібліятэку", + "ItemAddedWithName": "{0} даданы ў бібліятэку", "MessageApplicationUpdated": "Сервер Jellyfin абноўлены", - "NotificationOptionApplicationUpdateInstalled": "Абнаўленне прыкладання ўсталявана", + "NotificationOptionApplicationUpdateInstalled": "Абнаўленне праграмы ўсталявана", "PluginInstalledWithName": "{0} быў усталяваны", "UserCreatedWithName": "Карыстальнік {0} быў створаны", "Albums": "Альбомы", - "Application": "Прыкладанне", - "AuthenticationSucceededWithUserName": "{0} паспяхова аўтэнтыфікаваны", + "Application": "Праграма", + "AuthenticationSucceededWithUserName": "{0} паспяхова аўтарызаваны", "Channels": "Каналы", "ChapterNameValue": "Раздзел {0}", "Collections": "Калекцыі", @@ -29,18 +29,18 @@ "HeaderAlbumArtists": "Выканаўцы альбома", "LabelRunningTimeValue": "Працягласць: {0}", "HomeVideos": "Хатнія відэа", - "ItemRemovedWithName": "{0} быў выдалены з бібліятэкі", - "MessageApplicationUpdatedTo": "Сервер Jellyfin абноўлены да {0}", + "ItemRemovedWithName": "{0} выдалены з бібліятэкі", + "MessageApplicationUpdatedTo": "Сервер Jellyfin абноўлены да версіі {0}", "Movies": "Фільмы", "Music": "Музыка", "MusicVideos": "Музычныя кліпы", - "NameInstallFailed": "Устаноўка {0} не атрымалася", + "NameInstallFailed": "Усталяванне {0} не атрымалася", "NameSeasonNumber": "Сезон {0}", - "NotificationOptionApplicationUpdateAvailable": "Даступна абнаўленне прыкладання", + "NotificationOptionApplicationUpdateAvailable": "Даступна абнаўленне праграмы", "NotificationOptionPluginInstalled": "Плагін усталяваны", - "NotificationOptionPluginUpdateInstalled": "Абнаўленне плагіна усталявана", + "NotificationOptionPluginUpdateInstalled": "Абнаўленне плагіна ўсталявана", "NotificationOptionServerRestartRequired": "Патрабуецца перазапуск сервера", - "Photos": "Фатаграфіі", + "Photos": "Фотаздымкі", "Plugin": "Плагін", "PluginUninstalledWithName": "{0} быў выдалены", "PluginUpdatedWithName": "{0} быў абноўлены", @@ -54,16 +54,16 @@ "Artists": "Выканаўцы", "UserOfflineFromDevice": "{0} адлучыўся ад {1}", "UserPolicyUpdatedWithName": "Палітыка карыстальніка абноўлена для {0}", - "TaskCleanActivityLogDescription": "Выдаляе старэйшыя за зададзены ўзрост запісы ў журнале актыўнасці.", + "TaskCleanActivityLogDescription": "Выдаляе запісы старэйшыя за зададзены ўзрост ў журнале актыўнасці.", "TaskRefreshChapterImagesDescription": "Стварае мініяцюры для відэа, якія маюць раздзелы.", "TaskCleanLogsDescription": "Выдаляе файлы журналу, якім больш за {0} дзён.", - "TaskUpdatePluginsDescription": "Спампоўвае і ўсталёўвае абнаўленні для плагінаў, якія настроены на аўтаматычнае абнаўленне.", + "TaskUpdatePluginsDescription": "Спампоўвае і ўсталёўвае абнаўленні для плагінаў, якія сканфігураваныя на аўтаматычнае абнаўленне.", "TaskRefreshChannelsDescription": "Абнаўляе інфармацыю аб інтэрнэт-канале.", - "TaskDownloadMissingSubtitlesDescription": "Шукае ў інтэрнэце адсутныя субтытры на аснове канфігурацыі метададзеных.", - "TaskOptimizeDatabaseDescription": "Ушчыльняе базу дадзеных і скарачае вольную прастору. Выкананне гэтай задачы пасля сканавання бібліятэкі або ўнясення іншых змяненняў, якія прадугледжваюць мадыфікацыю базы дадзеных, можа палепшыць прадукцыйнасць.", + "TaskDownloadMissingSubtitlesDescription": "Шукае ў інтэрнэце адсутныя субцітры на аснове канфігурацыі метададзеных.", + "TaskOptimizeDatabaseDescription": "Ушчыльняе базу дадзеных і скарачае вольную прастору. Выкананне гэтай задачы пасля сканавання бібліятэкі або ўнясення іншых зменаў, якія прадугледжваюць мадыфікацыю базы дадзеных, можа палепшыць выдайнасць.", "TaskKeyframeExtractor": "Экстрактар ключавых кадраў", - "TasksApplicationCategory": "Прыкладанне", - "AppDeviceValues": "Прыкладанне: {0}, Прылада: {1}", + "TasksApplicationCategory": "Праграма", + "AppDeviceValues": "Праграма: {0}, Прылада: {1}", "Books": "Кнігі", "CameraImageUploadedFrom": "Новая выява камеры была загружана з {0}", "DeviceOfflineWithName": "{0} адлучыўся", @@ -74,7 +74,7 @@ "HeaderFavoriteArtists": "Абраныя выканаўцы", "HearingImpaired": "Са слабым слыхам", "Inherit": "Атрымаць у спадчыну", - "MessageNamedServerConfigurationUpdatedWithValue": "Канфігурацыя сервера {0} абноўлена", + "MessageNamedServerConfigurationUpdatedWithValue": "Канфігурацыя сервера (секцыя {0}) абноўлена", "MessageServerConfigurationUpdated": "Канфігурацыя сервера абноўлена", "MixedContent": "Змешаны змест", "NameSeasonUnknown": "Невядомы сезон", @@ -92,48 +92,48 @@ "NotificationOptionVideoPlaybackStopped": "Прайграванне відэа спынена", "ScheduledTaskFailedWithName": "{0} не атрымалася", "ScheduledTaskStartedWithName": "{0} пачалося", - "ServerNameNeedsToBeRestarted": "{0} трэба перазапусціць", + "ServerNameNeedsToBeRestarted": "{0} патрабуе перазапуску", "Shows": "Шоу", "StartupEmbyServerIsLoading": "Jellyfin Server загружаецца. Калі ласка, паўтарыце спробу крыху пазней.", "SubtitleDownloadFailureFromForItem": "Не атрымалася спампаваць субтытры з {0} для {1}", - "TvShows": "ТБ-шоу", + "TvShows": "Тэлепраграма", "Undefined": "Нявызначана", "UserLockedOutWithName": "Карыстальнік {0} быў заблакіраваны", "UserOnlineFromDevice": "{0} падключаны з {1}", "UserPasswordChangedWithName": "Пароль быў зменены для карыстальніка {0}", - "UserStartedPlayingItemWithValues": "{0} грае {1} на {2}", + "UserStartedPlayingItemWithValues": "{0} прайграваецца {1} на {2}", "UserStoppedPlayingItemWithValues": "{0} скончыў прайграванне {1} на {2}", "ValueHasBeenAddedToLibrary": "{0} быў дададзены ў вашу медыятэку", "ValueSpecialEpisodeName": "Спецэпізод - {0}", "VersionNumber": "Версія {0}", "TasksMaintenanceCategory": "Абслугоўванне", - "TasksLibraryCategory": "Медыятэка", + "TasksLibraryCategory": "Бібліятэка", "TasksChannelsCategory": "Інтэрнэт-каналы", "TaskCleanActivityLog": "Ачысціць журнал актыўнасці", "TaskCleanCache": "Ачысціць кэш", "TaskCleanCacheDescription": "Выдаляе файлы кэша, якія больш не патрэбныя сістэме.", - "TaskRefreshChapterImages": "Выняць выявы раздзелаў", - "TaskRefreshLibrary": "Сканіраваць медыятэку", - "TaskRefreshLibraryDescription": "Сканіруе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метададзеныя.", - "TaskCleanLogs": "Ачысціць часопіс", - "TaskRefreshPeople": "Абнавіць людзей", + "TaskRefreshChapterImages": "Вынуць выявы раздзелаў", + "TaskRefreshLibrary": "Сканаваць бібліятэку", + "TaskRefreshLibraryDescription": "Скануе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метададзеныя.", + "TaskCleanLogs": "Ачысціць журнал", + "TaskRefreshPeople": "Абнавіць выканаўцаў", "TaskRefreshPeopleDescription": "Абнаўленне метаданых для акцёраў і рэжысёраў у вашай медыятэцы.", "TaskUpdatePlugins": "Абнавіць плагіны", "TaskCleanTranscode": "Ачысціць каталог перакадзіравання", "TaskCleanTranscodeDescription": "Выдаляе перакадзіраваныя файлы, старэйшыя за адзін дзень.", "TaskRefreshChannels": "Абнавіць каналы", - "TaskDownloadMissingSubtitles": "Спампаваць адсутныя субтытры", - "TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных спісаў прайгравання HLS. Гэта задача можа працаваць у працягу доўгага часу.", - "TaskRefreshTrickplayImages": "Стварыце выявы Trickplay", - "TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках.", - "TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і спісы прайгравання", - "TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і спісаў прайгравання, якія больш не існуюць.", - "TaskAudioNormalizationDescription": "Сканіруе файлы на прадмет нармалізацыі гуку.", + "TaskDownloadMissingSubtitles": "Спампаваць адсутныя субцітры", + "TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных плэй-лістоў HLS. Гэта задача можа працягнуцца шмат часу.", + "TaskRefreshTrickplayImages": "Стварыць выявы Trickplay", + "TaskRefreshTrickplayImagesDescription": "Стварае перадпрагляды відэаролікаў для Trickplay у падключаных бібліятэках.", + "TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і плэй-лісты", + "TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і плэй-лістоў, якія больш не існуюць.", + "TaskAudioNormalizationDescription": "Скануе файлы на прадмет нармалізацыі гуку.", "TaskAudioNormalization": "Нармалізацыя гуку", "TaskExtractMediaSegmentsDescription": "Выдае або атрымлівае медыясегменты з убудоў з падтрымкай MediaSegment.", "TaskMoveTrickplayImagesDescription": "Перамяшчае існуючыя файлы trickplay у адпаведнасці з наладамі бібліятэкі.", - "TaskDownloadMissingLyrics": "Спампаваць зніклыя тэксты песень", - "TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песень", + "TaskDownloadMissingLyrics": "Спампаваць адсутныя тэксты песняў", + "TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песняў", "TaskExtractMediaSegments": "Сканіраванне медыя-сегмента", "TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay", "CleanupUserDataTask": "Задача па ачыстцы дадзеных карыстальніка", -- cgit v1.2.3 From 98f5e21bb8db171e01cd6fdfd902822e7913267c Mon Sep 17 00:00:00 2001 From: JPVenson Date: Tue, 23 Sep 2025 00:31:21 +0300 Subject: Fix groupings not applied (#14826) --- .../Library/UserDataManager.cs | 2 +- .../Item/BaseItemRepository.cs | 94 ++++++++++++---------- 2 files changed, 54 insertions(+), 42 deletions(-) (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index a83ba1570b..72c8d7a9d2 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -238,7 +238,7 @@ namespace Emby.Server.Implementations.Library /// public UserItemData? GetUserData(User user, BaseItem item) { - return item.UserData.Where(e => e.UserId.Equals(user.Id)).Select(Map).FirstOrDefault() ?? new UserItemData() + return item.UserData?.Where(e => e.UserId.Equals(user.Id)).Select(Map).FirstOrDefault() ?? new UserItemData() { Key = item.GetUserDataKeys()[0], }; diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 971297d55f..c2e6e7feb1 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -271,7 +271,7 @@ public sealed class BaseItemRepository result.TotalRecordCount = dbQuery.Count(); } - dbQuery = ApplyGroupingFilter(dbQuery, filter); + dbQuery = ApplyGroupingFilter(context, dbQuery, filter); dbQuery = ApplyQueryPaging(dbQuery, filter); result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray(); @@ -290,7 +290,7 @@ public sealed class BaseItemRepository dbQuery = TranslateQuery(dbQuery, context, filter); - dbQuery = ApplyGroupingFilter(dbQuery, filter); + dbQuery = ApplyGroupingFilter(context, dbQuery, filter); dbQuery = ApplyQueryPaging(dbQuery, filter); return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray(); @@ -332,7 +332,7 @@ public sealed class BaseItemRepository var mainquery = PrepareItemQuery(context, filter); mainquery = TranslateQuery(mainquery, context, filter); mainquery = mainquery.Where(g => g.DateCreated >= subqueryGrouped.Min(s => s.MaxDateCreated)); - mainquery = ApplyGroupingFilter(mainquery, filter); + mainquery = ApplyGroupingFilter(context, mainquery, filter); mainquery = ApplyQueryPaging(mainquery, filter); return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray(); @@ -369,36 +369,50 @@ public sealed class BaseItemRepository return query.ToArray(); } - private IQueryable ApplyGroupingFilter(IQueryable dbQuery, InternalItemsQuery filter) + private IQueryable ApplyGroupingFilter(JellyfinDbContext context, IQueryable dbQuery, InternalItemsQuery filter) { // This whole block is needed to filter duplicate entries on request // for the time being it cannot be used because it would destroy the ordering // this results in "duplicate" responses for queries that try to lookup individual series or multiple versions but // for that case the invoker has to run a DistinctBy(e => e.PresentationUniqueKey) on their own - // var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter); - // if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey) - // { - // dbQuery = ApplyOrder(dbQuery, filter); - // dbQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(e => e.First()); - // } - // else if (enableGroupByPresentationUniqueKey) - // { - // dbQuery = ApplyOrder(dbQuery, filter); - // dbQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.First()); - // } - // else if (filter.GroupBySeriesPresentationUniqueKey) - // { - // dbQuery = ApplyOrder(dbQuery, filter); - // dbQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.First()); - // } - // else - // { - // dbQuery = dbQuery.Distinct(); - // dbQuery = ApplyOrder(dbQuery, filter); - // } - dbQuery = dbQuery.Distinct(); - dbQuery = ApplyOrder(dbQuery, filter); + var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter); + if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey) + { + var tempQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(e => e.FirstOrDefault()).Select(e => e!.Id); + dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id)); + dbQuery = ApplyOrder(dbQuery, filter); + } + else if (enableGroupByPresentationUniqueKey) + { + var tempQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.FirstOrDefault()).Select(e => e!.Id); + dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id)); + dbQuery = ApplyOrder(dbQuery, filter); + } + else if (filter.GroupBySeriesPresentationUniqueKey) + { + var tempQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.FirstOrDefault()).Select(e => e!.Id); + dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id)); + dbQuery = ApplyOrder(dbQuery, filter); + } + else + { + dbQuery = dbQuery.Distinct(); + dbQuery = ApplyOrder(dbQuery, filter); + } + + dbQuery = dbQuery.Include(e => e.TrailerTypes) + .Include(e => e.Provider) + .Include(e => e.LockedFields) + .Include(e => e.UserData); + + if (filter.DtoOptions.EnableImages) + { + dbQuery = dbQuery.Include(e => e.Images); + } + + // dbQuery = dbQuery.Distinct(); + // dbQuery = ApplyOrder(dbQuery, filter); return dbQuery; } @@ -426,8 +440,8 @@ public sealed class BaseItemRepository private IQueryable ApplyQueryFilter(IQueryable dbQuery, JellyfinDbContext context, InternalItemsQuery filter) { dbQuery = TranslateQuery(dbQuery, context, filter); - dbQuery = ApplyOrder(dbQuery, filter); - dbQuery = ApplyGroupingFilter(dbQuery, filter); + // dbQuery = ApplyOrder(dbQuery, filter); + dbQuery = ApplyGroupingFilter(context, dbQuery, filter); dbQuery = ApplyQueryPaging(dbQuery, filter); return dbQuery; } @@ -435,16 +449,7 @@ public sealed class BaseItemRepository private IQueryable PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter) { IQueryable dbQuery = context.BaseItems.AsNoTracking(); - dbQuery = dbQuery.AsSingleQuery() - .Include(e => e.TrailerTypes) - .Include(e => e.Provider) - .Include(e => e.LockedFields) - .Include(e => e.UserData); - - if (filter.DtoOptions.EnableImages) - { - dbQuery = dbQuery.Include(e => e.Images); - } + dbQuery = dbQuery.AsSingleQuery(); return dbQuery; } @@ -729,13 +734,20 @@ public sealed class BaseItemRepository } using var context = _dbProvider.CreateDbContext(); - var item = PrepareItemQuery(context, new() + var dbQuery = PrepareItemQuery(context, new() { DtoOptions = new() { EnableImages = true } - }).FirstOrDefault(e => e.Id == id); + }); + dbQuery = dbQuery.Include(e => e.TrailerTypes) + .Include(e => e.Provider) + .Include(e => e.LockedFields) + .Include(e => e.UserData) + .Include(e => e.Images); + + var item = dbQuery.FirstOrDefault(e => e.Id == id); if (item is null) { return null; -- cgit v1.2.3 From 42003ca9d2178be165b7d4c671bbe7084e106e39 Mon Sep 17 00:00:00 2001 From: Looooke Date: Mon, 22 Sep 2025 17:39:04 -0400 Subject: Translated using Weblate (Alemannic) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/gsw/ --- Emby.Server.Implementations/Localization/Core/gsw.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/Localization/Core/gsw.json b/Emby.Server.Implementations/Localization/Core/gsw.json index b3ee2a4f67..f847d83d14 100644 --- a/Emby.Server.Implementations/Localization/Core/gsw.json +++ b/Emby.Server.Implementations/Localization/Core/gsw.json @@ -15,7 +15,7 @@ "Favorites": "Favorite", "Folders": "Ordner", "Genres": "Genre", - "HeaderAlbumArtists": "Album-Künstler", + "HeaderAlbumArtists": "Album-Künschtler", "HeaderContinueWatching": "weiter schauen", "HeaderFavoriteAlbums": "Lieblingsalben", "HeaderFavoriteArtists": "Lieblings-Künstler", -- cgit v1.2.3 From 5a6d9180fed81a30cb91ef3fed30176cd4402116 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Thu, 25 Sep 2025 00:20:30 +0300 Subject: Add People Dedup and multiple progress fixes (#14848) --- .../Data/CleanDatabaseScheduledTask.cs | 7 +- Emby.Server.Implementations/Dto/DtoService.cs | 27 +--- .../Library/LibraryManager.cs | 166 ++++++++++++++------- .../Library/Validators/PeopleValidator.cs | 22 ++- .../ScheduledTasks/Tasks/PeopleValidationTask.cs | 66 +++++++- .../Item/BaseItemRepository.cs | 20 ++- .../Item/PeopleRepository.cs | 34 +++-- .../Migrations/Routines/MigrateLibraryDb.cs | 4 +- MediaBrowser.Controller/Library/ILibraryManager.cs | 9 ++ .../Persistence/IItemRepository.cs | 12 +- .../PragmaConnectionInterceptor.cs | 2 +- 11 files changed, 257 insertions(+), 112 deletions(-) (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index 31ae82d6a3..676bb7f816 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -50,6 +50,8 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask _logger.LogDebug("Cleaning {Number} items with dead parents", numItems); + IProgress subProgress = new Progress((val) => progress.Report(val / 2)); + foreach (var itemId in itemIds) { cancellationToken.ThrowIfCancellationRequested(); @@ -95,9 +97,10 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask numComplete++; double percent = numComplete; percent /= numItems; - progress.Report(percent * 100); + subProgress.Report(percent * 100); } + subProgress = new Progress((val) => progress.Report((val / 2) + 50)); var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using (context.ConfigureAwait(false)) { @@ -105,7 +108,9 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask await using (transaction.ConfigureAwait(false)) { await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + subProgress.Report(50); await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + subProgress.Report(100); } } diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 0db1606ea5..c5dc3b054c 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1051,30 +1051,15 @@ namespace Emby.Server.Implementations.Dto // Include artists that are not in the database yet, e.g., just added via metadata editor // var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList(); - dto.ArtistItems = hasArtist.Artists - // .Except(foundArtists, new DistinctNameComparer()) + dto.ArtistItems = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]) + .Where(e => e.Value.Length > 0) .Select(i => { - // This should not be necessary but we're seeing some cases of it - if (string.IsNullOrEmpty(i)) - { - return null; - } - - var artist = _libraryManager.GetArtist(i, new DtoOptions(false) - { - EnableImages = false - }); - if (artist is not null) + return new NameGuidPair { - return new NameGuidPair - { - Name = artist.Name, - Id = artist.Id - }; - } - - return null; + Name = i.Key, + Id = i.Value.First().Id + }; }).Where(i => i is not null).ToArray(); } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index a66835dec0..102779729e 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -327,6 +327,45 @@ namespace Emby.Server.Implementations.Library DeleteItem(item, options, parent, notifyParentItem); } + public void DeleteItemsUnsafeFast(IEnumerable items) + { + var pathMaps = items.Select(e => (Item: e, InternalPath: GetInternalMetadataPaths(e), DeletePaths: e.GetDeletePaths())).ToArray(); + + foreach (var (item, internalPaths, pathsToDelete) in pathMaps) + { + foreach (var metadataPath in internalPaths) + { + if (!Directory.Exists(metadataPath)) + { + continue; + } + + _logger.LogDebug( + "Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", + item.GetType().Name, + item.Name ?? "Unknown name", + metadataPath, + item.Id); + + try + { + Directory.Delete(metadataPath, true); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting {MetadataPath}", metadataPath); + } + } + + foreach (var fileSystemInfo in pathsToDelete) + { + DeleteItemPath(item, false, fileSystemInfo); + } + } + + _itemRepository.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]); + } + public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem) { ArgumentNullException.ThrowIfNull(item); @@ -403,59 +442,7 @@ namespace Emby.Server.Implementations.Library foreach (var fileSystemInfo in item.GetDeletePaths()) { - if (Directory.Exists(fileSystemInfo.FullName) || File.Exists(fileSystemInfo.FullName)) - { - try - { - _logger.LogInformation( - "Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", - item.GetType().Name, - item.Name ?? "Unknown name", - fileSystemInfo.FullName, - item.Id); - - if (fileSystemInfo.IsDirectory) - { - Directory.Delete(fileSystemInfo.FullName, true); - } - else - { - File.Delete(fileSystemInfo.FullName); - } - } - catch (DirectoryNotFoundException) - { - _logger.LogInformation( - "Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", - item.GetType().Name, - item.Name ?? "Unknown name", - fileSystemInfo.FullName, - item.Id); - } - catch (FileNotFoundException) - { - _logger.LogInformation( - "File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", - item.GetType().Name, - item.Name ?? "Unknown name", - fileSystemInfo.FullName, - item.Id); - } - catch (IOException) - { - if (isRequiredForDelete) - { - throw; - } - } - catch (UnauthorizedAccessException) - { - if (isRequiredForDelete) - { - throw; - } - } - } + DeleteItemPath(item, isRequiredForDelete, fileSystemInfo); isRequiredForDelete = false; } @@ -463,17 +450,73 @@ namespace Emby.Server.Implementations.Library item.SetParent(null); - _itemRepository.DeleteItem(item.Id); + _itemRepository.DeleteItem([item.Id, .. children.Select(f => f.Id)]); _cache.TryRemove(item.Id, out _); foreach (var child in children) { - _itemRepository.DeleteItem(child.Id); _cache.TryRemove(child.Id, out _); } ReportItemRemoved(item, parent); } + private void DeleteItemPath(BaseItem item, bool isRequiredForDelete, FileSystemMetadata fileSystemInfo) + { + if (Directory.Exists(fileSystemInfo.FullName) || File.Exists(fileSystemInfo.FullName)) + { + try + { + _logger.LogInformation( + "Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", + item.GetType().Name, + item.Name ?? "Unknown name", + fileSystemInfo.FullName, + item.Id); + + if (fileSystemInfo.IsDirectory) + { + Directory.Delete(fileSystemInfo.FullName, true); + } + else + { + File.Delete(fileSystemInfo.FullName); + } + } + catch (DirectoryNotFoundException) + { + _logger.LogInformation( + "Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", + item.GetType().Name, + item.Name ?? "Unknown name", + fileSystemInfo.FullName, + item.Id); + } + catch (FileNotFoundException) + { + _logger.LogInformation( + "File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", + item.GetType().Name, + item.Name ?? "Unknown name", + fileSystemInfo.FullName, + item.Id); + } + catch (IOException) + { + if (isRequiredForDelete) + { + throw; + } + } + catch (UnauthorizedAccessException) + { + if (isRequiredForDelete) + { + throw; + } + } + } + } + private bool IsInternalItem(BaseItem item) { if (!item.IsFileProtocol) @@ -990,6 +1033,11 @@ namespace Emby.Server.Implementations.Library return GetArtist(name, new DtoOptions(true)); } + public IReadOnlyDictionary GetArtists(IReadOnlyList names) + { + return _itemRepository.FindArtists(names); + } + public MusicArtist GetArtist(string name, DtoOptions options) { return CreateItemByName(MusicArtist.GetPath, name, options); @@ -1115,18 +1163,24 @@ namespace Emby.Server.Implementations.Library cancellationToken: cancellationToken).ConfigureAwait(false); // Quickly scan CollectionFolders for changes + var toDelete = new List(); foreach (var child in rootFolder.Children!.OfType()) { // If the user has somehow deleted the collection directory, remove the metadata from the database. if (child is CollectionFolder collectionFolder && !Directory.Exists(collectionFolder.Path)) { - _itemRepository.DeleteItem(collectionFolder.Id); + toDelete.Add(collectionFolder.Id); } else { await child.RefreshMetadata(cancellationToken).ConfigureAwait(false); } } + + if (toDelete.Count > 0) + { + _itemRepository.DeleteItem(toDelete.ToArray()); + } } private async Task PerformLibraryValidation(IProgress progress, CancellationToken cancellationToken) diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs index b7fd24fa5c..f9a6f0d19e 100644 --- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs @@ -1,5 +1,5 @@ using System; -using System.Globalization; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; @@ -55,6 +55,8 @@ public class PeopleValidator var numPeople = people.Count; + IProgress subProgress = new Progress((val) => progress.Report(val / 2)); + _logger.LogDebug("Will refresh {Amount} people", numPeople); foreach (var person in people) @@ -92,7 +94,7 @@ public class PeopleValidator double percent = numComplete; percent /= numPeople; - progress.Report(100 * percent); + subProgress.Report(100 * percent); } var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery @@ -102,17 +104,13 @@ public class PeopleValidator IsLocked = false }); - foreach (var item in deadEntities) - { - _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name); + subProgress = new Progress((val) => progress.Report((val / 2) + 50)); - _libraryManager.DeleteItem( - item, - new DeleteOptions - { - DeleteFileLocation = false - }, - false); + var i = 0; + foreach (var item in deadEntities.Chunk(500)) + { + _libraryManager.DeleteItemsUnsafeFast(item); + subProgress.Report(100f / deadEntities.Count * (i++ * 100)); } progress.Report(100); diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs index 18162ad2fc..6e4e5c7808 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs @@ -1,10 +1,14 @@ using System; +using System.Buffers; using System.Collections.Generic; +using System.Linq; 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.EntityFrameworkCore; namespace Emby.Server.Implementations.ScheduledTasks.Tasks; @@ -15,16 +19,19 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask { private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; + private readonly IDbContextFactory _dbContextFactory; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. - public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization) + /// Instance of the interface. + public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization, IDbContextFactory dbContextFactory) { _libraryManager = libraryManager; _localization = localization; + _dbContextFactory = dbContextFactory; } /// @@ -62,8 +69,61 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask } /// - public Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) + public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { - return _libraryManager.ValidatePeopleAsync(progress, cancellationToken); + IProgress subProgress = new Progress((val) => progress.Report(val / 2)); + await _libraryManager.ValidatePeopleAsync(subProgress, cancellationToken).ConfigureAwait(false); + + subProgress = new Progress((val) => progress.Report((val / 2) + 50)); + var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (context.ConfigureAwait(false)) + { + var dupQuery = context.Peoples + .GroupBy(e => new { e.Name, e.PersonType }) + .Where(e => e.Count() > 1) + .Select(e => e.Select(f => f.Id).ToArray()); + + var total = dupQuery.Count(); + + const int PartitionSize = 100; + var iterator = 0; + int itemCounter; + var buffer = ArrayPool.Shared.Rent(PartitionSize)!; + try + { + do + { + itemCounter = 0; + await foreach (var item in dupQuery + .Take(PartitionSize) + .AsAsyncEnumerable() + .WithCancellation(cancellationToken) + .ConfigureAwait(false)) + { + buffer[itemCounter++] = item; + } + + for (int i = 0; i < itemCounter; i++) + { + var item = buffer[i]; + var reference = item[0]; + var dups = item[1..]; + await context.PeopleBaseItemMap.WhereOneOrMany(dups, e => e.PeopleId) + .ExecuteUpdateAsync(e => e.SetProperty(f => f.PeopleId, reference), cancellationToken) + .ConfigureAwait(false); + await context.Peoples.Where(e => dups.Contains(e.Id)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + subProgress.Report(100f / total * ((iterator * PartitionSize) + i)); + } + + iterator++; + } while (itemCounter == PartitionSize && !cancellationToken.IsCancellationRequested); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + + subProgress.Report(100); + } } } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index a34e95c4de..68260fbf0e 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -99,11 +99,11 @@ public sealed class BaseItemRepository } /// - public void DeleteItem(Guid id) + public void DeleteItem(params IReadOnlyList ids) { - if (id.IsEmpty() || id.Equals(PlaceholderId)) + if (ids is null || ids.Count == 0 || ids.Any(f => f.Equals(PlaceholderId))) { - throw new ArgumentException("Guid can't be empty or the placeholder id.", nameof(id)); + throw new ArgumentException("Guid can't be empty or the placeholder id.", nameof(ids)); } using var context = _dbProvider.CreateDbContext(); @@ -111,7 +111,7 @@ public sealed class BaseItemRepository var date = (DateTime?)DateTime.UtcNow; - var relatedItems = TraverseHirachyDown(id, context).ToArray(); + var relatedItems = ids.SelectMany(f => TraverseHirachyDown(f, context)).ToArray(); // Remove any UserData entries for the placeholder item that would conflict with the UserData // being detached from the item being deleted. This is necessary because, during an update, @@ -2538,4 +2538,16 @@ public sealed class BaseItemRepository return folderList; } + + /// + public IReadOnlyDictionary FindArtists(IReadOnlyList artistNames) + { + using var dbContext = _dbProvider.CreateDbContext(); + + var artists = dbContext.BaseItems.Where(e => e.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!) + .Where(e => artistNames.Contains(e.Name)) + .ToArray(); + + return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Cast().ToArray()); + } } diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index 24afaea550..0f423cf5d3 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -74,20 +74,34 @@ public class PeopleRepository(IDbContextFactory dbProvider, I /// public void UpdatePeople(Guid itemId, IReadOnlyList people) { - // TODO: yes for __SOME__ reason there can be duplicates. - people = people.DistinctBy(e => e.Id).ToArray(); - var personids = people.Select(f => f.Id); + // multiple metadata providers can provide the _same_ person + people = people.DistinctBy(e => e.Name + "-" + e.Type).ToArray(); + var personKeys = people.Select(e => e.Name + "-" + e.Type).ToArray(); using var context = _dbProvider.CreateDbContext(); using var transaction = context.Database.BeginTransaction(); - var existingPersons = context.Peoples.Where(p => personids.Contains(p.Id)).Select(f => f.Id).ToArray(); - context.Peoples.AddRange(people.Where(e => !existingPersons.Contains(e.Id)).Select(Map)); + var existingPersons = context.Peoples.Select(e => new + { + item = e, + SelectionKey = e.Name + "-" + e.PersonType + }) + .Where(p => personKeys.Contains(p.SelectionKey)) + .Select(f => f.item) + .ToArray(); + + var toAdd = people + .Where(e => !existingPersons.Any(f => f.Name == e.Name && f.PersonType == e.Type.ToString())) + .Select(Map); + context.Peoples.AddRange(toAdd); context.SaveChanges(); - var maps = context.PeopleBaseItemMap.Where(e => e.ItemId == itemId).ToList(); + var personsEntities = toAdd.Concat(existingPersons).ToArray(); + + var existingMaps = context.PeopleBaseItemMap.Include(e => e.People).Where(e => e.ItemId == itemId).ToList(); foreach (var person in people) { - var existingMap = maps.FirstOrDefault(e => e.PeopleId == person.Id); + var entityPerson = personsEntities.First(e => e.Name == person.Name && e.PersonType == person.Type.ToString()); + var existingMap = existingMaps.FirstOrDefault(e => e.People.Name == person.Name && e.Role == person.Role); if (existingMap is null) { var sortOrder = (person.SortOrder ?? context.PeopleBaseItemMap.Where(e => e.ItemId == itemId).Max(e => e.SortOrder) ?? 0) + 1; @@ -96,7 +110,7 @@ public class PeopleRepository(IDbContextFactory dbProvider, I Item = null!, ItemId = itemId, People = null!, - PeopleId = person.Id, + PeopleId = entityPerson.Id, ListOrder = sortOrder, SortOrder = sortOrder, Role = person.Role @@ -105,11 +119,11 @@ public class PeopleRepository(IDbContextFactory dbProvider, I else { // person mapping already exists so remove from list - maps.Remove(existingMap); + existingMaps.Remove(existingMap); } } - context.PeopleBaseItemMap.RemoveRange(maps); + context.PeopleBaseItemMap.RemoveRange(existingMaps); context.SaveChanges(); transaction.Commit(); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index ca8e1054e5..b8f416a766 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -337,9 +337,9 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine } var entity = GetPerson(reader); - if (!peopleCache.TryGetValue(entity.Name, out var personCache)) + if (!peopleCache.TryGetValue(entity.Name + "|" + entity.PersonType, out var personCache)) { - peopleCache[entity.Name] = personCache = (entity, []); + peopleCache[entity.Name + "|" + entity.PersonType] = personCache = (entity, []); } if (reader.TryGetString(2, out var role)) diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index b72d1d0b4c..fcc5ed672a 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -336,6 +336,13 @@ namespace MediaBrowser.Controller.Library /// Options to use for deletion. void DeleteItem(BaseItem item, DeleteOptions options); + /// + /// Deletes items that are not having any children like Actors. + /// + /// Items to delete. + /// In comparison to this method skips a lot of steps assuming there are no children to recusively delete nor does it define the special handling for channels and alike. + public void DeleteItemsUnsafeFast(IEnumerable items); + /// /// Deletes the item. /// @@ -624,6 +631,8 @@ namespace MediaBrowser.Controller.Library QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query); + IReadOnlyDictionary GetArtists(IReadOnlyList names); + QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query); QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query); diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index e17dc38f7f..0026ab2b5f 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; @@ -22,8 +23,8 @@ public interface IItemRepository /// /// Deletes the item. /// - /// The identifier. - void DeleteItem(Guid id); + /// The identifier to delete. + void DeleteItem(params IReadOnlyList ids); /// /// Saves the items. @@ -122,4 +123,11 @@ public interface IItemRepository /// Whever the check should be done recursive. Warning expensive operation. /// A value indicating whever all children has been played. bool GetIsPlayed(User user, Guid id, bool recursive); + + /// + /// Gets all artist matches from the db. + /// + /// The names of the artists. + /// A map of the artist name and the potential matches. + IReadOnlyDictionary FindArtists(IReadOnlyList artistNames); } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/PragmaConnectionInterceptor.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/PragmaConnectionInterceptor.cs index 47e44d97b9..fd2b9bd05b 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/PragmaConnectionInterceptor.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/PragmaConnectionInterceptor.cs @@ -43,7 +43,7 @@ public class PragmaConnectionInterceptor : DbConnectionInterceptor _customPragma = customPragma; InitialCommand = BuildCommandText(); - _logger.LogInformation("SQLITE connection pragma command set to: \r\n {PragmaCommand}", InitialCommand); + _logger.LogInformation("SQLITE connection pragma command set to: \r\n{PragmaCommand}", InitialCommand); } private string? InitialCommand { get; set; } -- cgit v1.2.3 From e6cd73df03c7665c7d099bcb2776334fa4909821 Mon Sep 17 00:00:00 2001 From: daswesen123 Date: Fri, 26 Sep 2025 18:12:04 -0400 Subject: Translated using Weblate (English (Pirate)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/en@pirate/ --- .../Localization/Core/pr.json | 72 +++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json index f7d1b112e1..9076b9c878 100644 --- a/Emby.Server.Implementations/Localization/Core/pr.json +++ b/Emby.Server.Implementations/Localization/Core/pr.json @@ -43,5 +43,75 @@ "NameInstallFailed": "Ye couldn't bring {0} aboard yer ship", "MessageApplicationUpdatedTo": "Yer Map of the Seas has been scribbled with {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Yer Map Drawer has been rescribbled to {0}", - "MessageServerConfigurationUpdated": "Yer Map drawer has been rescribbled" + "MessageServerConfigurationUpdated": "Yer Map drawer has been rescribbled", + "Inherit": "Carry on what be passed along", + "Latest": "Newfangled", + "Movies": "Moving pictures", + "NewVersionIsAvailable": "A fresh build o’ Jellyfin Server be waitin’ fer ye to fetch.", + "NotificationOptionPluginInstalled": "Plugin nailed down", + "NotificationOptionVideoPlayback": "Video playback be underway", + "ScheduledTaskFailedWithName": "{0} ran aground", + "StartupEmbyServerIsLoading": "Jellyfin Server be preparin’ the ship. Try yer luck again soon.", + "UserOfflineFromDevice": "{0} severed ties with {1}", + "UserDownloadingItemWithValues": "{0} be haulin’ in {1}", + "UserStartedPlayingItemWithValues": "{0} be playin’ {1} aboard {2}", + "ValueHasBeenAddedToLibrary": "{0} be stashed in yer treasure trove", + "TaskCleanCacheDescription": "Wipes away cache cargo no longer called fer.", + "TaskCleanLogsDescription": "Clears the logbook o’ entries older than {0} days.", + "TaskRefreshPeopleDescription": "Refreshes the charts fer actors an’ directors in yer Treasure Trove.", + "UserLockedOutWithName": "Matey {0} be denied boarding", + "TaskAudioNormalization": "Steadyin’ the shanties", + "TaskAudioNormalizationDescription": "Scans files fer shanty steadiyin’ data.", + "HeaderRecordingGroups": "Loggin' Groups", + "MusicVideos": "Shanty films", + "Playlists": "Lists o’ plunder", + "Plugin": "Extra sail", + "NotificationOptionVideoPlaybackStopped": "Video playback dropped anchor", + "NameSeasonNumber": "Saga {0}", + "NameSeasonUnknown": "Saga be Lost", + "NotificationOptionApplicationUpdateAvailable": "A fresh build awaits", + "NotificationOptionApplicationUpdateInstalled": "App upgrade be aboard", + "NotificationOptionAudioPlayback": "Audio playback be rollin", + "NotificationOptionAudioPlaybackStopped": "Audio playback dropped anchor", + "NotificationOptionCameraImageUploaded": "Spyglass shot be hoisted", + "NotificationOptionInstallationFailed": "Install be wrecked", + "NotificationOptionNewLibraryContent": "Fresh plunder ready to claim", + "NotificationOptionPluginError": "Plugin ran aground", + "NotificationOptionPluginUninstalled": "Plugin cast overboard", + "NotificationOptionPluginUpdateInstalled": "Plugin patched ‘n ready", + "NotificationOptionServerRestartRequired": "Server be due fer a restart", + "NotificationOptionTaskFailed": "Set chore went overboard", + "TaskRefreshLibraryDescription": "Searches the Treasure Trove fer new plunder ‘n updates the charts.", + "PluginInstalledWithName": "{0} nailed down", + "TaskCleanLogs": "Swab the Log Hold", + "TaskRefreshPeople": "Freshen the Mateys", + "PluginUninstalledWithName": "{0} sent t’ Davy Jones", + "PluginUpdatedWithName": "{0} patched ‘n ready", + "ProviderValue": "Supplier o’ goods: {0}", + "ScheduledTaskStartedWithName": "{0} set sail", + "ServerNameNeedsToBeRestarted": "{0} be cravin’ a restart", + "Shows": "Sagas", + "SubtitleDownloadFailureFromForItem": "Subtitles be sunk fetchin’ from {0} fer {1}", + "Sync": "Match the tides", + "System": "The ship’s works", + "TvShows": "TV Sagas", + "Undefined": "Uncharted", + "User": "Matey", + "UserCreatedWithName": "Matey {0} joined the crew", + "UserDeletedWithName": "Matey {0} cast overboard", + "UserOnlineFromDevice": "{0} be aboard ship from {1}", + "UserPasswordChangedWithName": "New passphrase set fer Matey {0}", + "UserPolicyUpdatedWithName": "Ship rules be changed fer {0}", + "UserStoppedPlayingItemWithValues": "{0} be done playin’ {1} on {2", + "ValueSpecialEpisodeName": "Special Tale – {0}", + "VersionNumber": "Edition {0}", + "TasksMaintenanceCategory": "Hull patchin’", + "TasksLibraryCategory": "Treasure Trove", + "TasksApplicationCategory": "Ship", + "TaskCleanActivityLog": "Clear the Ship’s Log", + "TaskCleanActivityLogDescription": "Purges ship’s logs older than the chosen time.", + "TaskCleanCache": "Sweep the Cache Chest", + "TaskRefreshChapterImages": "Claim chapter portraits", + "TaskRefreshChapterImagesDescription": "Paints wee portraits fer videos that own chapters.", + "TaskRefreshLibrary": "Scan the Treasure Trove" } -- cgit v1.2.3 From baa7f5f0b02df1ca755addac661d21286510512b Mon Sep 17 00:00:00 2001 From: Nicolas N Date: Sun, 28 Sep 2025 02:16:22 -0400 Subject: Translated using Weblate (Haitian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ht/ --- .../Localization/Core/ht.json | 61 +++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/Localization/Core/ht.json b/Emby.Server.Implementations/Localization/Core/ht.json index 4fcba99e90..f927d3173a 100644 --- a/Emby.Server.Implementations/Localization/Core/ht.json +++ b/Emby.Server.Implementations/Localization/Core/ht.json @@ -1,3 +1,62 @@ { - "Books": "liv" + "Books": "Liv", + "TasksLibraryCategory": "Libreri", + "Albums": "Albòm yo", + "Artists": "Atis yo", + "Application": "Aplikasyon", + "Channels": "Kanal yo", + "ChapterNameValue": "Chapit {0}", + "Default": "Defo", + "DeviceOnlineWithName": "{0} konekte", + "DeviceOfflineWithName": "{0} dekonekte", + "External": "Extèn", + "Collections": "Koleksyon yo", + "Favorites": "Pi Renmen", + "Folders": "Dosye", + "Genres": "Jan yo", + "Forced": "Fòse", + "HeaderAlbumArtists": "Albòm Atis", + "HeaderContinueWatching": "Kontinye Kade", + "HeaderFavoriteAlbums": "Albòm Pi Renmen", + "HeaderFavoriteArtists": "Atis Pi Renmen", + "HeaderFavoriteEpisodes": "Epizòd Pi Renmen", + "HeaderFavoriteShows": "Emisyon Pi Renmen", + "HeaderFavoriteSongs": "Mizik Pi Renmen", + "HeaderLiveTV": "Televizyon an Direk", + "HeaderNextUp": "Pwochen an", + "HomeVideos": "Videyo Lakay", + "Latest": "Pi Resan", + "MessageApplicationUpdated": "Sèvè Jellyfin met a jou", + "MessageApplicationUpdatedTo": "Sèvè Jellyfin met a jou sou {0}", + "Movies": "Fim", + "MixedContent": "Kontni Melanje", + "Music": "Mizik", + "MusicVideos": "Videyo Mizik", + "NameInstallFailed": "{0} enstalasyon fe fayit", + "NameSeasonNumber": "Sezon {0}", + "NameSeasonUnknown": "Sezon Enkoni", + "NotificationOptionCameraImageUploaded": "Imaj Kamera telechaje", + "NotificationOptionInstallationFailed": "Enstalasyon echwe", + "Photos": "Foto", + "PluginInstalledWithName": "{0} te enstale", + "PluginUninstalledWithName": "{0} te dezenstale", + "PluginUpdatedWithName": "{0} te mi a jou", + "ScheduledTaskFailedWithName": "{0} echwe", + "ScheduledTaskStartedWithName": "{0} komanse", + "Songs": "Mizik yo", + "Shows": "Emisyon yo", + "System": "Sistèm", + "TvShows": "Emisyon Tele", + "User": "Itilizatè", + "UserCreatedWithName": "Itilizatè {0} kreye", + "UserDeletedWithName": "Itilizatè {0} a efase", + "UserDownloadingItemWithValues": "{0} ap telechaje {1}", + "UserOfflineFromDevice": "{0} dekonekte de {1}", + "UserStartedPlayingItemWithValues": "{0} ap jwe {1} sou {2}", + "UserStoppedPlayingItemWithValues": "{0} fin jwe {1} sou {2}", + "UserPasswordChangedWithName": "Modpas la chanje pou Itilizatè {0}", + "ValueSpecialEpisodeName": "Spesyal - {0}", + "VersionNumber": "Vesyon {0}", + "TasksApplicationCategory": "Aplikasyon", + "TasksMaintenanceCategory": "Antretyen" } -- cgit v1.2.3 From d6cebf1e67f30063d572519e31b0459b58f3e4b0 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Wed, 1 Oct 2025 19:26:48 -0400 Subject: Add tag filtering and random sorting to GetSimilarItems (#14918) --- Emby.Server.Implementations/Library/LibraryManager.cs | 2 +- Jellyfin.Api/Controllers/LibraryController.cs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 102779729e..ef497726e2 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -528,7 +528,7 @@ namespace Emby.Server.Implementations.Library { Genre => _configurationManager.ApplicationPaths.GenrePath, MusicArtist => _configurationManager.ApplicationPaths.ArtistsPath, - MusicGenre => _configurationManager.ApplicationPaths.GenrePath, + MusicGenre => _configurationManager.ApplicationPaths.MusicGenrePath, Person => _configurationManager.ApplicationPaths.PeoplePath, Studio => _configurationManager.ApplicationPaths.StudioPath, Year => _configurationManager.ApplicationPaths.YearPath, diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index b18d7e05d4..4c9cc2b1e8 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -779,12 +779,14 @@ public class LibraryController : BaseJellyfinApiController var query = new InternalItemsQuery(user) { Genres = item.Genres, + Tags = item.Tags, Limit = limit, IncludeItemTypes = includeItemTypes.ToArray(), DtoOptions = dtoOptions, EnableTotalRecordCount = !isMovie ?? true, EnableGroupByMetadataKey = isMovie ?? false, - ExcludeItemIds = [itemId] + ExcludeItemIds = [itemId], + OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)] }; // ExcludeArtistIds -- cgit v1.2.3