From 711e649e354262d066d5cca6e1694aa369e59289 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Wed, 30 Jul 2025 19:41:34 +0000 Subject: [PATCH 1/4] Also migrate IsFolder --- Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index e25c52786b..13e641c1d3 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -105,7 +105,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId, DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, SeasonName, SeasonId, SeriesId, PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate, - ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName, CleanName, UnratedType FROM TypedBaseItems + ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName, CleanName, UnratedType, IsFolder FROM TypedBaseItems """; using (new TrackedMigrationStep("Loading TypedBaseItems", _logger)) { @@ -1167,6 +1167,11 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine entity.UnratedType = unratedType; } + if (reader.TryGetBoolean(index++, out var isFolder)) + { + entity.IsFolder = isFolder; + } + var baseItem = BaseItemRepository.DeserializeBaseItem(entity, _logger, null, false); var dataKeys = baseItem.GetUserDataKeys(); userDataKeys.AddRange(dataKeys); From a1eb04dc0b1449f52bbb52be16a0fff3b8806523 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Wed, 30 Jul 2025 19:58:56 +0000 Subject: [PATCH 2/4] Add full migration for IsFolder flag --- .../Migrations/Routines/MigrateLibraryDb.cs | 3 + .../Migrations/Routines/ReseedFolderFlag.cs | 73 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index 13e641c1d3..e04a2737a6 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -90,6 +90,9 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine operation.JellyfinDbContext.AncestorIds.ExecuteDelete(); } + // notify the other migration to just silently abort because the fix has been applied here already. + ReseedFolderFlag.RerunGuardFlag = true; + var legacyBaseItemWithUserKeys = new Dictionary(); connection.Open(); diff --git a/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs b/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs new file mode 100644 index 0000000000..d8c5cc0383 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs @@ -0,0 +1,73 @@ +#pragma warning disable RS0030 // Do not use banned APIs +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Emby.Server.Implementations.Data; +using Jellyfin.Database.Implementations; +using Jellyfin.Server.ServerSetupApp; +using MediaBrowser.Controller; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations.Routines; + +[JellyfinMigration("2025-07-30T21:50:00", nameof(ReseedFolderFlag))] +[JellyfinMigrationBackup(JellyfinDb = true)] +internal class ReseedFolderFlag : IAsyncMigrationRoutine +{ + private const string DbFilename = "library.db.old"; + + private readonly IStartupLogger _logger; + private readonly IServerApplicationPaths _paths; + private readonly IDbContextFactory _provider; + + public ReseedFolderFlag( + IStartupLogger startupLogger, + IDbContextFactory provider, + IServerApplicationPaths paths) + { + _logger = startupLogger; + _provider = provider; + _paths = paths; + } + + internal static bool RerunGuardFlag { get; set; } = false; + + public async Task PerformAsync(CancellationToken cancellationToken) + { + if (RerunGuardFlag) + { + _logger.LogInformation("Migration is skipped because it does not apply."); + return; + } + + _logger.LogInformation("Migrating the IsFolder flag from library.db.old may take a while, do not stop Jellyfin."); + + var dataPath = _paths.DataPath; + var libraryDbPath = Path.Combine(dataPath, DbFilename); + if (!File.Exists(libraryDbPath)) + { + _logger.LogError("Cannot migrate IsFolder flag from {LibraryDb} as it does not exist. This migration expects the MigrateLibraryDb to run first.", libraryDbPath); + return; + } + + var dbContext = await _provider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly"); + var queryResult = connection.Query( +""" + SELECT key FROM TypedBaseItems + + WHERE IsFolder = true +"""); + foreach (var entity in queryResult) + { + var id = entity.GetGuid(0); + await dbContext.BaseItems.Where(e => e.Id == id).ExecuteUpdateAsync(e => e.SetProperty(f => f.IsFolder, true), cancellationToken).ConfigureAwait(false); + } + } + } +} From ef733c5acebcb2f14428e180f83ab24e5c9b3c45 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Wed, 30 Jul 2025 20:10:26 +0000 Subject: [PATCH 3/4] use guid instead --- Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs b/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs index d8c5cc0383..371c2e8bef 100644 --- a/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs +++ b/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs @@ -59,7 +59,7 @@ internal class ReseedFolderFlag : IAsyncMigrationRoutine using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly"); var queryResult = connection.Query( """ - SELECT key FROM TypedBaseItems + SELECT guid FROM TypedBaseItems WHERE IsFolder = true """); From c8d2f436608c631e94461e70c3a7511031d32f58 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Wed, 30 Jul 2025 20:14:24 +0000 Subject: [PATCH 4/4] Add logging --- .../Migrations/Routines/ReseedFolderFlag.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs b/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs index 371c2e8bef..502763ac09 100644 --- a/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs +++ b/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs @@ -58,14 +58,15 @@ internal class ReseedFolderFlag : IAsyncMigrationRoutine { using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly"); var queryResult = connection.Query( -""" - SELECT guid FROM TypedBaseItems - - WHERE IsFolder = true -"""); - foreach (var entity in queryResult) + """ + SELECT guid FROM TypedBaseItems + WHERE IsFolder = true + """) + .Select(entity => entity.GetGuid(0)) + .ToList(); + _logger.LogInformation("Migrating the IsFolder flag for {Count} items.", queryResult.Count); + foreach (var id in queryResult) { - var id = entity.GetGuid(0); await dbContext.BaseItems.Where(e => e.Id == id).ExecuteUpdateAsync(e => e.SetProperty(f => f.IsFolder, true), cancellationToken).ConfigureAwait(false); } }