2
0
Эх сурвалжийг харах

Unified migration handling (#13950)

JPVenson 1 сар өмнө
parent
commit
e66c76fc34
40 өөрчлөгдсөн 555 нэмэгдсэн , 528 устгасан
  1. 0 15
      Emby.Server.Implementations/ApplicationHost.cs
  2. 31 0
      Jellyfin.Server/Migrations/IAsyncMigrationRoutine.cs
  3. 2 0
      Jellyfin.Server/Migrations/IDatabaseMigrationRoutine.cs
  4. 0 32
      Jellyfin.Server/Migrations/IMigrationRoutine.cs
  5. 65 0
      Jellyfin.Server/Migrations/JellyfinMigrationAttribute.cs
  6. 219 0
      Jellyfin.Server/Migrations/JellyfinMigrationService.cs
  7. 0 204
      Jellyfin.Server/Migrations/MigrationRunner.cs
  8. 0 20
      Jellyfin.Server/Migrations/MigrationsFactory.cs
  9. 0 24
      Jellyfin.Server/Migrations/MigrationsListStore.cs
  10. 3 9
      Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs
  11. 3 9
      Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs
  12. 3 9
      Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs
  13. 1 9
      Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs
  14. 3 9
      Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs
  15. 3 9
      Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs
  16. 3 9
      Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs
  17. 3 9
      Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
  18. 3 9
      Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
  19. 3 9
      Jellyfin.Server/Migrations/Routines/FixAudioData.cs
  20. 3 9
      Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
  21. 3 9
      Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
  22. 3 9
      Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
  23. 3 9
      Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
  24. 1 9
      Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs
  25. 19 12
      Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
  26. 1 9
      Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
  27. 3 9
      Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
  28. 3 9
      Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs
  29. 4 10
      Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
  30. 3 9
      Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs
  31. 3 9
      Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs
  32. 3 9
      Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
  33. 3 9
      Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs
  34. 3 9
      Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs
  35. 51 0
      Jellyfin.Server/Migrations/Stages/CodeMigration.cs
  36. 26 0
      Jellyfin.Server/Migrations/Stages/JellyfinMigrationStageTypes.cs
  37. 16 0
      Jellyfin.Server/Migrations/Stages/MigrationStage.cs
  38. 49 4
      Jellyfin.Server/Program.cs
  39. 5 0
      src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs
  40. 5 0
      tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs

+ 0 - 15
Emby.Server.Implementations/ApplicationHost.cs

@@ -580,21 +580,6 @@ namespace Emby.Server.Implementations
         /// <returns>A task representing the service initialization operation.</returns>
         public async Task InitializeServices(IConfiguration startupConfig)
         {
-            var factory = Resolve<IDbContextFactory<JellyfinDbContext>>();
-            var provider = Resolve<IJellyfinDatabaseProvider>();
-            provider.DbContextFactory = factory;
-
-            var jellyfinDb = await factory.CreateDbContextAsync().ConfigureAwait(false);
-            await using (jellyfinDb.ConfigureAwait(false))
-            {
-                if ((await jellyfinDb.Database.GetPendingMigrationsAsync().ConfigureAwait(false)).Any())
-                {
-                    Logger.LogInformation("There are pending EFCore migrations in the database. Applying... (This may take a while, do not stop Jellyfin)");
-                    await jellyfinDb.Database.MigrateAsync().ConfigureAwait(false);
-                    Logger.LogInformation("EFCore migrations applied successfully");
-                }
-            }
-
             var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
             await localizationManager.LoadAll().ConfigureAwait(false);
 

+ 31 - 0
Jellyfin.Server/Migrations/IAsyncMigrationRoutine.cs

@@ -0,0 +1,31 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Jellyfin.Server.Migrations;
+
+/// <summary>
+/// Interface that describes a migration routine.
+/// </summary>
+internal interface IAsyncMigrationRoutine
+{
+    /// <summary>
+    /// Execute the migration routine.
+    /// </summary>
+    /// <param name="cancellationToken">A cancellation token triggered if the migration should be aborted.</param>
+    /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+    public Task PerformAsync(CancellationToken cancellationToken);
+}
+
+/// <summary>
+/// Interface that describes a migration routine.
+/// </summary>
+[Obsolete("Use IAsyncMigrationRoutine instead")]
+internal interface IMigrationRoutine
+{
+    /// <summary>
+    /// Execute the migration routine.
+    /// </summary>
+    [Obsolete("Use IAsyncMigrationRoutine.PerformAsync instead")]
+    public void Perform();
+}

+ 2 - 0
Jellyfin.Server/Migrations/IDatabaseMigrationRoutine.cs

@@ -7,6 +7,8 @@ namespace Jellyfin.Server.Migrations;
 /// <summary>
 /// Defines a migration that operates on the Database.
 /// </summary>
+#pragma warning disable CS0618 // Type or member is obsolete
 internal interface IDatabaseMigrationRoutine : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
 {
 }

+ 0 - 32
Jellyfin.Server/Migrations/IMigrationRoutine.cs

@@ -1,32 +0,0 @@
-using System;
-using Jellyfin.Server.Implementations;
-using Microsoft.EntityFrameworkCore.Internal;
-
-namespace Jellyfin.Server.Migrations
-{
-    /// <summary>
-    /// Interface that describes a migration routine.
-    /// </summary>
-    internal interface IMigrationRoutine
-    {
-        /// <summary>
-        /// Gets the unique id for this migration. This should never be modified after the migration has been created.
-        /// </summary>
-        public Guid Id { get; }
-
-        /// <summary>
-        /// Gets the display name of the migration.
-        /// </summary>
-        public string Name { get; }
-
-        /// <summary>
-        /// Gets a value indicating whether to perform migration on a new install.
-        /// </summary>
-        public bool PerformOnNewInstall { get; }
-
-        /// <summary>
-        /// Execute the migration routine.
-        /// </summary>
-        public void Perform();
-    }
-}

+ 65 - 0
Jellyfin.Server/Migrations/JellyfinMigrationAttribute.cs

@@ -0,0 +1,65 @@
+#pragma warning disable CA1019 // Define accessors for attribute arguments
+
+using System;
+using System.Globalization;
+using Jellyfin.Server.Migrations.Stages;
+
+namespace Jellyfin.Server.Migrations;
+
+/// <summary>
+/// Declares an class as an migration with its set metadata.
+/// </summary>
+[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
+public sealed class JellyfinMigrationAttribute : Attribute
+{
+    /// <summary>
+    /// Initializes a new instance of the <see cref="JellyfinMigrationAttribute"/> class.
+    /// </summary>
+    /// <param name="order">The ordering this migration should be applied to. Must be a valid DateTime ISO8601 formatted string.</param>
+    /// <param name="name">The name of this Migration.</param>
+    public JellyfinMigrationAttribute(string order, string name) : this(order, name, null)
+    {
+    }
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="JellyfinMigrationAttribute"/> class for legacy migrations.
+    /// </summary>
+    /// <param name="order">The ordering this migration should be applied to. Must be a valid DateTime ISO8601 formatted string.</param>
+    /// <param name="name">The name of this Migration.</param>
+    /// <param name="key">[ONLY FOR LEGACY MIGRATIONS]The unique key of this migration. Must be a valid Guid formatted string.</param>
+    public JellyfinMigrationAttribute(string order, string name, string? key)
+    {
+        Order = DateTime.Parse(order, CultureInfo.InvariantCulture);
+        Name = name;
+        Stage = JellyfinMigrationStageTypes.AppInitialisation;
+        if (key is not null)
+        {
+            Key = Guid.Parse(key);
+        }
+    }
+
+    /// <summary>
+    /// Gets or Sets a value indicating whether the annoated migration should be executed on a fresh install.
+    /// </summary>
+    public bool RunMigrationOnSetup { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the stage the annoated migration should be executed at. Defaults to <see cref="JellyfinMigrationStageTypes.CoreInitialisaition"/>.
+    /// </summary>
+    public JellyfinMigrationStageTypes Stage { get; set; } = JellyfinMigrationStageTypes.CoreInitialisaition;
+
+    /// <summary>
+    /// Gets the ordering of the migration.
+    /// </summary>
+    public DateTime Order { get; }
+
+    /// <summary>
+    /// Gets the name of the migration.
+    /// </summary>
+    public string Name { get; }
+
+    /// <summary>
+    /// Gets the Legacy Key of the migration. Not required for new Migrations.
+    /// </summary>
+    public Guid? Key { get; }
+}

+ 219 - 0
Jellyfin.Server/Migrations/JellyfinMigrationService.cs

@@ -0,0 +1,219 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Server.Implementations.Serialization;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Server.Migrations.Stages;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations;
+
+/// <summary>
+/// Handles Migration of the Jellyfin data structure.
+/// </summary>
+internal class JellyfinMigrationService
+{
+    private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory;
+    private readonly ILoggerFactory _loggerFactory;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="JellyfinMigrationService"/> class.
+    /// </summary>
+    /// <param name="dbContextFactory">Provides access to the jellyfin database.</param>
+    /// <param name="loggerFactory">The logger factory.</param>
+    public JellyfinMigrationService(IDbContextFactory<JellyfinDbContext> dbContextFactory, ILoggerFactory loggerFactory)
+    {
+        _dbContextFactory = dbContextFactory;
+        _loggerFactory = loggerFactory;
+#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<JellyfinMigrationAttribute>()))
+            .Where(e => e.Metadata != null)
+            .GroupBy(e => e.Metadata!.Stage)
+            .Select(f =>
+            {
+                var stage = new MigrationStage(f.Key);
+                foreach (var item in f)
+                {
+                    stage.Add(new(item.Type, item.Metadata!));
+                }
+
+                return stage;
+            })];
+#pragma warning restore CS0618 // Type or member is obsolete
+    }
+
+    private interface IInternalMigration
+    {
+        Task PerformAsync(ILogger logger);
+    }
+
+    private HashSet<MigrationStage> Migrations { get; set; }
+
+    public async Task CheckFirstTimeRunOrMigration(IApplicationPaths appPaths)
+    {
+        var logger = _loggerFactory.CreateLogger<JellyfinMigrationService>();
+        logger.LogInformation("Initialise Migration service.");
+        var xmlSerializer = new MyXmlSerializer();
+        var serverConfig = File.Exists(appPaths.SystemConfigurationFilePath)
+            ? (ServerConfiguration)xmlSerializer.DeserializeFromFile(typeof(ServerConfiguration), appPaths.SystemConfigurationFilePath)!
+            : new ServerConfiguration();
+        if (!serverConfig.IsStartupWizardCompleted)
+        {
+            logger.LogInformation("System initialisation detected. Seed data.");
+            var flatApplyMigrations = Migrations.SelectMany(e => e.Where(f => !f.Metadata.RunMigrationOnSetup)).ToArray();
+
+            var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                var historyRepository = dbContext.GetService<IHistoryRepository>();
+
+                await historyRepository.CreateIfNotExistsAsync().ConfigureAwait(false);
+                var appliedMigrations = await dbContext.Database.GetAppliedMigrationsAsync().ConfigureAwait(false);
+                var startupScripts = flatApplyMigrations
+                    .Where(e => !appliedMigrations.Any(f => f != e.BuildCodeMigrationId()))
+                    .Select(e => (Migration: e.Metadata, Script: historyRepository.GetInsertScript(new HistoryRow(e.BuildCodeMigrationId(), GetJellyfinVersion()))))
+                    .ToArray();
+                foreach (var item in startupScripts)
+                {
+                    logger.LogInformation("Seed migration {Key}-{Name}.", item.Migration.Key, item.Migration.Name);
+                    await dbContext.Database.ExecuteSqlRawAsync(item.Script).ConfigureAwait(false);
+                }
+            }
+
+            logger.LogInformation("Migration system initialisation completed.");
+        }
+        else
+        {
+            // migrate any existing migration.xml files
+            var migrationConfigPath = Path.Join(appPaths.ConfigurationDirectoryPath, "migrations.xml");
+            var migrationOptions = File.Exists(migrationConfigPath)
+                 ? (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)!
+                 : null;
+            if (migrationOptions != null && migrationOptions.Applied.Count > 0)
+            {
+                logger.LogInformation("Old migration style migration.xml detected. Migrate now.");
+                var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
+                await using (dbContext.ConfigureAwait(false))
+                {
+                    var historyRepository = dbContext.GetService<IHistoryRepository>();
+                    var appliedMigrations = await dbContext.Database.GetAppliedMigrationsAsync().ConfigureAwait(false);
+                    var oldMigrations = Migrations.SelectMany(e => e)
+                        .Where(e => migrationOptions.Applied.Any(f => f.Id.Equals(e.Metadata.Key!.Value))) // this is a legacy migration that will always have its own ID.
+                        .Where(e => !appliedMigrations.Contains(e.BuildCodeMigrationId()))
+                        .ToArray();
+                    var startupScripts = oldMigrations.Select(e => (Migration: e.Metadata, Script: historyRepository.GetInsertScript(new HistoryRow(e.BuildCodeMigrationId(), GetJellyfinVersion()))));
+                    foreach (var item in startupScripts)
+                    {
+                        logger.LogInformation("Migrate migration {Key}-{Name}.", item.Migration.Key, item.Migration.Name);
+                        await dbContext.Database.ExecuteSqlRawAsync(item.Script).ConfigureAwait(false);
+                    }
+
+                    logger.LogInformation("Rename old migration.xml to migration.xml.backup");
+                    File.Move(migrationConfigPath, Path.ChangeExtension(migrationConfigPath, ".xml.backup"), true);
+                }
+            }
+        }
+    }
+
+    public async Task MigrateStepAsync(JellyfinMigrationStageTypes stage, IServiceProvider? serviceProvider)
+    {
+        var logger = _loggerFactory.CreateLogger<JellyfinMigrationService>();
+        logger.LogInformation("Migrate stage {Stage}.", stage);
+        ICollection<CodeMigration> migrationStage = (Migrations.FirstOrDefault(e => e.Stage == stage) as ICollection<CodeMigration>) ?? [];
+
+        var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
+        await using (dbContext.ConfigureAwait(false))
+        {
+            var historyRepository = dbContext.GetService<IHistoryRepository>();
+            var migrationsAssembly = dbContext.GetService<IMigrationsAssembly>();
+            var appliedMigrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
+            var pendingCodeMigrations = migrationStage
+                .Where(e => appliedMigrations.All(f => f.MigrationId != e.BuildCodeMigrationId()))
+                .Select(e => (Key: e.BuildCodeMigrationId(), Migration: new InternalCodeMigration(e, serviceProvider, dbContext)))
+                .ToArray();
+
+            (string Key, InternalDatabaseMigration Migration)[] pendingDatabaseMigrations = [];
+            if (stage is JellyfinMigrationStageTypes.CoreInitialisaition)
+            {
+                pendingDatabaseMigrations = migrationsAssembly.Migrations.Where(f => appliedMigrations.All(e => e.MigrationId != f.Key))
+                   .Select(e => (Key: e.Key, Migration: new InternalDatabaseMigration(e, dbContext)))
+                   .ToArray();
+            }
+
+            (string Key, IInternalMigration Migration)[] pendingMigrations = [.. pendingCodeMigrations, .. pendingDatabaseMigrations];
+            logger.LogInformation("There are {Pending} migrations for stage {Stage}.", pendingCodeMigrations.Length, stage);
+            var migrations = pendingMigrations.OrderBy(e => e.Key).ToArray();
+            foreach (var item in migrations)
+            {
+                try
+                {
+                    logger.LogInformation("Perform migration {Name}", item.Key);
+                    await item.Migration.PerformAsync(_loggerFactory.CreateLogger(item.GetType().Name)).ConfigureAwait(false);
+                    logger.LogInformation("Migration {Name} was successfully applied", item.Key);
+                }
+                catch (Exception ex)
+                {
+                    logger.LogCritical(ex, "Migration {Name} failed", item.Key);
+                    throw;
+                }
+            }
+        }
+    }
+
+    private static string GetJellyfinVersion()
+    {
+        return Assembly.GetEntryAssembly()!.GetName().Version!.ToString();
+    }
+
+    private class InternalCodeMigration : IInternalMigration
+    {
+        private readonly CodeMigration _codeMigration;
+        private readonly IServiceProvider? _serviceProvider;
+        private JellyfinDbContext _dbContext;
+
+        public InternalCodeMigration(CodeMigration codeMigration, IServiceProvider? serviceProvider, JellyfinDbContext dbContext)
+        {
+            _codeMigration = codeMigration;
+            _serviceProvider = serviceProvider;
+            _dbContext = dbContext;
+        }
+
+        public async Task PerformAsync(ILogger logger)
+        {
+            await _codeMigration.Perform(_serviceProvider, CancellationToken.None).ConfigureAwait(false);
+
+            var historyRepository = _dbContext.GetService<IHistoryRepository>();
+            var createScript = historyRepository.GetInsertScript(new HistoryRow(_codeMigration.BuildCodeMigrationId(), GetJellyfinVersion()));
+            await _dbContext.Database.ExecuteSqlRawAsync(createScript).ConfigureAwait(false);
+        }
+    }
+
+    private class InternalDatabaseMigration : IInternalMigration
+    {
+        private readonly JellyfinDbContext _jellyfinDbContext;
+        private KeyValuePair<string, TypeInfo> _databaseMigrationInfo;
+
+        public InternalDatabaseMigration(KeyValuePair<string, TypeInfo> databaseMigrationInfo, JellyfinDbContext jellyfinDbContext)
+        {
+            _databaseMigrationInfo = databaseMigrationInfo;
+            _jellyfinDbContext = jellyfinDbContext;
+        }
+
+        public async Task PerformAsync(ILogger logger)
+        {
+            var migrator = _jellyfinDbContext.GetService<IMigrator>();
+            await migrator.MigrateAsync(_databaseMigrationInfo.Key).ConfigureAwait(false);
+        }
+    }
+}

+ 0 - 204
Jellyfin.Server/Migrations/MigrationRunner.cs

@@ -1,204 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations;
-using Emby.Server.Implementations.Serialization;
-using Jellyfin.Database.Implementations;
-using Jellyfin.Server.Implementations;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Model.Configuration;
-using Microsoft.EntityFrameworkCore.Storage;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-
-namespace Jellyfin.Server.Migrations
-{
-    /// <summary>
-    /// The class that knows which migrations to apply and how to apply them.
-    /// </summary>
-    public sealed class MigrationRunner
-    {
-        /// <summary>
-        /// The list of known pre-startup migrations, in order of applicability.
-        /// </summary>
-        private static readonly Type[] _preStartupMigrationTypes =
-        {
-            typeof(PreStartupRoutines.CreateNetworkConfiguration),
-            typeof(PreStartupRoutines.MigrateMusicBrainzTimeout),
-            typeof(PreStartupRoutines.MigrateNetworkConfiguration),
-            typeof(PreStartupRoutines.MigrateEncodingOptions),
-            typeof(PreStartupRoutines.RenameEnableGroupingIntoCollections)
-        };
-
-        /// <summary>
-        /// The list of known migrations, in order of applicability.
-        /// </summary>
-        private static readonly Type[] _migrationTypes =
-        {
-            typeof(Routines.DisableTranscodingThrottling),
-            typeof(Routines.CreateUserLoggingConfigFile),
-            typeof(Routines.MigrateActivityLogDb),
-            typeof(Routines.RemoveDuplicateExtras),
-            typeof(Routines.AddDefaultPluginRepository),
-            typeof(Routines.MigrateUserDb),
-            typeof(Routines.ReaddDefaultPluginRepository),
-            typeof(Routines.MigrateDisplayPreferencesDb),
-            typeof(Routines.RemoveDownloadImagesInAdvance),
-            typeof(Routines.MigrateAuthenticationDb),
-            typeof(Routines.FixPlaylistOwner),
-            typeof(Routines.AddDefaultCastReceivers),
-            typeof(Routines.UpdateDefaultPluginRepository),
-            typeof(Routines.FixAudioData),
-            typeof(Routines.RemoveDuplicatePlaylistChildren),
-            typeof(Routines.MigrateLibraryDb),
-            typeof(Routines.MoveExtractedFiles),
-            typeof(Routines.MigrateRatingLevels),
-            typeof(Routines.MoveTrickplayFiles),
-            typeof(Routines.MigrateKeyframeData),
-        };
-
-        /// <summary>
-        /// Run all needed migrations.
-        /// </summary>
-        /// <param name="host">CoreAppHost that hosts current version.</param>
-        /// <param name="loggerFactory">Factory for making the logger.</param>
-        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
-        public static async Task Run(CoreAppHost host, ILoggerFactory loggerFactory)
-        {
-            var logger = loggerFactory.CreateLogger<MigrationRunner>();
-            var migrations = _migrationTypes
-                .Select(m => ActivatorUtilities.CreateInstance(host.ServiceProvider, m))
-                .OfType<IMigrationRoutine>()
-                .ToArray();
-
-            var migrationOptions = host.ConfigurationManager.GetConfiguration<MigrationOptions>(MigrationsListStore.StoreKey);
-            HandleStartupWizardCondition(migrations, migrationOptions, host.ConfigurationManager.Configuration.IsStartupWizardCompleted, logger);
-            await PerformMigrations(migrations, migrationOptions, options => host.ConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, options), logger, host.ServiceProvider.GetRequiredService<IJellyfinDatabaseProvider>())
-                .ConfigureAwait(false);
-        }
-
-        /// <summary>
-        /// Run all needed pre-startup migrations.
-        /// </summary>
-        /// <param name="appPaths">Application paths.</param>
-        /// <param name="loggerFactory">Factory for making the logger.</param>
-        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
-        public static async Task RunPreStartup(ServerApplicationPaths appPaths, ILoggerFactory loggerFactory)
-        {
-            var logger = loggerFactory.CreateLogger<MigrationRunner>();
-            var migrations = _preStartupMigrationTypes
-                .Select(m => Activator.CreateInstance(m, appPaths, loggerFactory))
-                .OfType<IMigrationRoutine>()
-                .ToArray();
-
-            var xmlSerializer = new MyXmlSerializer();
-            var migrationConfigPath = Path.Join(appPaths.ConfigurationDirectoryPath, MigrationsListStore.StoreKey.ToLowerInvariant() + ".xml");
-            var migrationOptions = File.Exists(migrationConfigPath)
-                 ? (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)!
-                 : new MigrationOptions();
-
-            // We have to deserialize it manually since the configuration manager may overwrite it
-            var serverConfig = File.Exists(appPaths.SystemConfigurationFilePath)
-                ? (ServerConfiguration)xmlSerializer.DeserializeFromFile(typeof(ServerConfiguration), appPaths.SystemConfigurationFilePath)!
-                : new ServerConfiguration();
-
-            HandleStartupWizardCondition(migrations, migrationOptions, serverConfig.IsStartupWizardCompleted, logger);
-            await PerformMigrations(migrations, migrationOptions, options => xmlSerializer.SerializeToFile(options, migrationConfigPath), logger, null).ConfigureAwait(false);
-        }
-
-        private static void HandleStartupWizardCondition(IEnumerable<IMigrationRoutine> migrations, MigrationOptions migrationOptions, bool isStartWizardCompleted, ILogger logger)
-        {
-            if (isStartWizardCompleted)
-            {
-                return;
-            }
-
-            // If startup wizard is not finished, this is a fresh install.
-            var onlyOldInstalls = migrations.Where(m => !m.PerformOnNewInstall).ToArray();
-            logger.LogInformation("Marking following migrations as applied because this is a fresh install: {@OnlyOldInstalls}", onlyOldInstalls.Select(m => m.Name));
-            migrationOptions.Applied.AddRange(onlyOldInstalls.Select(m => (m.Id, m.Name)));
-        }
-
-        private static async Task PerformMigrations(
-            IMigrationRoutine[] migrations,
-            MigrationOptions migrationOptions,
-            Action<MigrationOptions> saveConfiguration,
-            ILogger logger,
-            IJellyfinDatabaseProvider? jellyfinDatabaseProvider)
-        {
-            // save already applied migrations, and skip them thereafter
-            saveConfiguration(migrationOptions);
-            var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet();
-            var migrationsToBeApplied = migrations.Where(e => !appliedMigrationIds.Contains(e.Id)).ToArray();
-
-            string? migrationKey = null;
-            if (jellyfinDatabaseProvider is not null && migrationsToBeApplied.Any(f => f is IDatabaseMigrationRoutine))
-            {
-                logger.LogInformation("Performing database backup");
-                try
-                {
-                    migrationKey = await jellyfinDatabaseProvider.MigrationBackupFast(CancellationToken.None).ConfigureAwait(false);
-                    logger.LogInformation("Database backup with key '{BackupKey}' has been successfully created.", migrationKey);
-                }
-                catch (NotImplementedException)
-                {
-                    logger.LogWarning("Could not perform backup of database before migration because provider does not support it");
-                }
-            }
-
-            List<IMigrationRoutine> databaseMigrations = [];
-            try
-            {
-                foreach (var migrationRoutine in migrationsToBeApplied)
-                {
-                    logger.LogInformation("Applying migration '{Name}'", migrationRoutine.Name);
-                    var isDbMigration = migrationRoutine is IDatabaseMigrationRoutine;
-
-                    if (isDbMigration)
-                    {
-                        databaseMigrations.Add(migrationRoutine);
-                    }
-
-                    try
-                    {
-                        migrationRoutine.Perform();
-                    }
-                    catch (Exception ex)
-                    {
-                        logger.LogError(ex, "Could not apply migration '{Name}'", migrationRoutine.Name);
-                        throw;
-                    }
-
-                    // Mark the migration as completed
-                    logger.LogInformation("Migration '{Name}' applied successfully", migrationRoutine.Name);
-                    if (!isDbMigration)
-                    {
-                        migrationOptions.Applied.Add((migrationRoutine.Id, migrationRoutine.Name));
-                        saveConfiguration(migrationOptions);
-                        logger.LogDebug("Migration '{Name}' marked as applied in configuration.", migrationRoutine.Name);
-                    }
-                }
-            }
-            catch (Exception) when (migrationKey is not null && jellyfinDatabaseProvider is not null)
-            {
-                if (databaseMigrations.Count != 0)
-                {
-                    logger.LogInformation("Rolling back database as migrations reported failure.");
-                    await jellyfinDatabaseProvider.RestoreBackupFast(migrationKey, CancellationToken.None).ConfigureAwait(false);
-                }
-
-                throw;
-            }
-
-            foreach (var migrationRoutine in databaseMigrations)
-            {
-                migrationOptions.Applied.Add((migrationRoutine.Id, migrationRoutine.Name));
-                saveConfiguration(migrationOptions);
-                logger.LogDebug("Migration '{Name}' marked as applied in configuration.", migrationRoutine.Name);
-            }
-        }
-    }
-}

+ 0 - 20
Jellyfin.Server/Migrations/MigrationsFactory.cs

@@ -1,20 +0,0 @@
-using System.Collections.Generic;
-using MediaBrowser.Common.Configuration;
-
-namespace Jellyfin.Server.Migrations
-{
-    /// <summary>
-    /// A factory that can find a persistent file of the migration configuration, which lists all applied migrations.
-    /// </summary>
-    public class MigrationsFactory : IConfigurationFactory
-    {
-        /// <inheritdoc/>
-        public IEnumerable<ConfigurationStore> GetConfigurations()
-        {
-            return new[]
-            {
-                new MigrationsListStore()
-            };
-        }
-    }
-}

+ 0 - 24
Jellyfin.Server/Migrations/MigrationsListStore.cs

@@ -1,24 +0,0 @@
-using MediaBrowser.Common.Configuration;
-
-namespace Jellyfin.Server.Migrations
-{
-    /// <summary>
-    /// A configuration that lists all the migration routines that were applied.
-    /// </summary>
-    public class MigrationsListStore : ConfigurationStore
-    {
-        /// <summary>
-        /// The name of the configuration in the storage.
-        /// </summary>
-        public static readonly string StoreKey = "migrations";
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="MigrationsListStore"/> class.
-        /// </summary>
-        public MigrationsListStore()
-        {
-            ConfigurationType = typeof(MigrationOptions);
-            Key = StoreKey;
-        }
-    }
-}

+ 3 - 9
Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs

@@ -8,7 +8,10 @@ using Microsoft.Extensions.Logging;
 namespace Jellyfin.Server.Migrations.PreStartupRoutines;
 
 /// <inheritdoc />
+[JellyfinMigration("2025-04-20T00:00:00", nameof(CreateNetworkConfiguration), "9B354818-94D5-4B68-AC49-E35CB85F9D84", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)]
+#pragma warning disable CS0618 // Type or member is obsolete
 public class CreateNetworkConfiguration : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
 {
     private readonly ServerApplicationPaths _applicationPaths;
     private readonly ILogger<CreateNetworkConfiguration> _logger;
@@ -24,15 +27,6 @@ public class CreateNetworkConfiguration : IMigrationRoutine
         _logger = loggerFactory.CreateLogger<CreateNetworkConfiguration>();
     }
 
-    /// <inheritdoc />
-    public Guid Id => Guid.Parse("9B354818-94D5-4B68-AC49-E35CB85F9D84");
-
-    /// <inheritdoc />
-    public string Name => nameof(CreateNetworkConfiguration);
-
-    /// <inheritdoc />
-    public bool PerformOnNewInstall => false;
-
     /// <inheritdoc />
     public void Perform()
     {

+ 3 - 9
Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs

@@ -10,7 +10,10 @@ using Microsoft.Extensions.Logging;
 namespace Jellyfin.Server.Migrations.PreStartupRoutines;
 
 /// <inheritdoc />
+[JellyfinMigration("2025-04-20T03:00:00", nameof(MigrateEncodingOptions), "A8E61960-7726-4450-8F3D-82C12DAABBCB", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)]
+#pragma warning disable CS0618 // Type or member is obsolete
 public class MigrateEncodingOptions : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
 {
     private readonly ServerApplicationPaths _applicationPaths;
     private readonly ILogger<MigrateEncodingOptions> _logger;
@@ -26,15 +29,6 @@ public class MigrateEncodingOptions : IMigrationRoutine
         _logger = loggerFactory.CreateLogger<MigrateEncodingOptions>();
     }
 
-    /// <inheritdoc />
-    public Guid Id => Guid.Parse("A8E61960-7726-4450-8F3D-82C12DAABBCB");
-
-    /// <inheritdoc />
-    public string Name => nameof(MigrateEncodingOptions);
-
-    /// <inheritdoc />
-    public bool PerformOnNewInstall => false;
-
     /// <inheritdoc />
     public void Perform()
     {

+ 3 - 9
Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs

@@ -9,7 +9,10 @@ using Microsoft.Extensions.Logging;
 namespace Jellyfin.Server.Migrations.PreStartupRoutines;
 
 /// <inheritdoc />
+[JellyfinMigration("2025-04-20T02:00:00", nameof(MigrateMusicBrainzTimeout), "A6DCACF4-C057-4Ef9-80D3-61CEF9DDB4F0", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)]
+#pragma warning disable CS0618 // Type or member is obsolete
 public class MigrateMusicBrainzTimeout : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
 {
     private readonly ServerApplicationPaths _applicationPaths;
     private readonly ILogger<MigrateMusicBrainzTimeout> _logger;
@@ -25,15 +28,6 @@ public class MigrateMusicBrainzTimeout : IMigrationRoutine
         _logger = loggerFactory.CreateLogger<MigrateMusicBrainzTimeout>();
     }
 
-    /// <inheritdoc />
-    public Guid Id => Guid.Parse("A6DCACF4-C057-4Ef9-80D3-61CEF9DDB4F0");
-
-    /// <inheritdoc />
-    public string Name => nameof(MigrateMusicBrainzTimeout);
-
-    /// <inheritdoc />
-    public bool PerformOnNewInstall => false;
-
     /// <inheritdoc />
     public void Perform()
     {

+ 1 - 9
Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs

@@ -11,6 +11,7 @@ using Microsoft.Extensions.Logging;
 namespace Jellyfin.Server.Migrations.PreStartupRoutines;
 
 /// <inheritdoc />
+[JellyfinMigration("2025-04-20T01:00:00", nameof(MigrateNetworkConfiguration), "4FB5C950-1991-11EE-9B4B-0800200C9A66", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)]
 public class MigrateNetworkConfiguration : IMigrationRoutine
 {
     private readonly ServerApplicationPaths _applicationPaths;
@@ -27,15 +28,6 @@ public class MigrateNetworkConfiguration : IMigrationRoutine
         _logger = loggerFactory.CreateLogger<MigrateNetworkConfiguration>();
     }
 
-    /// <inheritdoc />
-    public Guid Id => Guid.Parse("4FB5C950-1991-11EE-9B4B-0800200C9A66");
-
-    /// <inheritdoc />
-    public string Name => nameof(MigrateNetworkConfiguration);
-
-    /// <inheritdoc />
-    public bool PerformOnNewInstall => false;
-
     /// <inheritdoc />
     public void Perform()
     {

+ 3 - 9
Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs

@@ -9,7 +9,10 @@ using Microsoft.Extensions.Logging;
 namespace Jellyfin.Server.Migrations.PreStartupRoutines;
 
 /// <inheritdoc />
+[JellyfinMigration("2025-04-20T04:00:00", nameof(RenameEnableGroupingIntoCollections), "E73B777D-CD5C-4E71-957A-B86B3660B7CF", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)]
+#pragma warning disable CS0618 // Type or member is obsolete
 public class RenameEnableGroupingIntoCollections : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
 {
     private readonly ServerApplicationPaths _applicationPaths;
     private readonly ILogger<RenameEnableGroupingIntoCollections> _logger;
@@ -25,15 +28,6 @@ public class RenameEnableGroupingIntoCollections : IMigrationRoutine
         _logger = loggerFactory.CreateLogger<RenameEnableGroupingIntoCollections>();
     }
 
-    /// <inheritdoc />
-    public Guid Id => Guid.Parse("E73B777D-CD5C-4E71-957A-B86B3660B7CF");
-
-    /// <inheritdoc />
-    public string Name => nameof(RenameEnableGroupingIntoCollections);
-
-    /// <inheritdoc />
-    public bool PerformOnNewInstall => false;
-
     /// <inheritdoc />
     public void Perform()
     {

+ 3 - 9
Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs

@@ -7,7 +7,10 @@ namespace Jellyfin.Server.Migrations.Routines;
 /// <summary>
 /// Migration to add the default cast receivers to the system config.
 /// </summary>
+[JellyfinMigration("2025-04-20T16:00:00", nameof(AddDefaultCastReceivers), "34A1A1C4-5572-418E-A2F8-32CDFE2668E8", RunMigrationOnSetup = true)]
+#pragma warning disable CS0618 // Type or member is obsolete
 public class AddDefaultCastReceivers : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
 {
     private readonly IServerConfigurationManager _serverConfigurationManager;
 
@@ -20,15 +23,6 @@ public class AddDefaultCastReceivers : IMigrationRoutine
         _serverConfigurationManager = serverConfigurationManager;
     }
 
-    /// <inheritdoc />
-    public Guid Id => new("34A1A1C4-5572-418E-A2F8-32CDFE2668E8");
-
-    /// <inheritdoc />
-    public string Name => "AddDefaultCastReceivers";
-
-    /// <inheritdoc />
-    public bool PerformOnNewInstall => true;
-
     /// <inheritdoc />
     public void Perform()
     {

+ 3 - 9
Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs

@@ -7,7 +7,10 @@ namespace Jellyfin.Server.Migrations.Routines
     /// <summary>
     /// Migration to initialize system configuration with the default plugin repository.
     /// </summary>
+    [JellyfinMigration("2025-04-20T09:00:00", nameof(AddDefaultPluginRepository), "EB58EBEE-9514-4B9B-8225-12E1A40020DF", RunMigrationOnSetup = true)]
+#pragma warning disable CS0618 // Type or member is obsolete
     public class AddDefaultPluginRepository : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
     {
         private readonly IServerConfigurationManager _serverConfigurationManager;
 
@@ -26,15 +29,6 @@ namespace Jellyfin.Server.Migrations.Routines
             _serverConfigurationManager = serverConfigurationManager;
         }
 
-        /// <inheritdoc/>
-        public Guid Id => Guid.Parse("EB58EBEE-9514-4B9B-8225-12E1A40020DF");
-
-        /// <inheritdoc/>
-        public string Name => "AddDefaultPluginRepository";
-
-        /// <inheritdoc/>
-        public bool PerformOnNewInstall => true;
-
         /// <inheritdoc/>
         public void Perform()
         {

+ 3 - 9
Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs

@@ -12,7 +12,10 @@ namespace Jellyfin.Server.Migrations.Routines
     /// If the deprecated logging.json file exists and has a custom config, it will be used as logging.user.json,
     /// otherwise a blank file will be created.
     /// </summary>
+    [JellyfinMigration("2025-04-20T06:00:00", nameof(CreateUserLoggingConfigFile), "EF103419-8451-40D8-9F34-D1A8E93A1679")]
+#pragma warning disable CS0618 // Type or member is obsolete
     internal class CreateUserLoggingConfigFile : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
     {
         /// <summary>
         /// File history for logging.json as existed during this migration creation. The contents for each has been minified.
@@ -42,15 +45,6 @@ namespace Jellyfin.Server.Migrations.Routines
             _appPaths = appPaths;
         }
 
-        /// <inheritdoc/>
-        public Guid Id => Guid.Parse("{EF103419-8451-40D8-9F34-D1A8E93A1679}");
-
-        /// <inheritdoc/>
-        public string Name => "CreateLoggingConfigHierarchy";
-
-        /// <inheritdoc/>
-        public bool PerformOnNewInstall => false;
-
         /// <inheritdoc/>
         public void Perform()
         {

+ 3 - 9
Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs

@@ -7,7 +7,10 @@ namespace Jellyfin.Server.Migrations.Routines
     /// <summary>
     /// Disable transcode throttling for all installations since it is currently broken for certain video formats.
     /// </summary>
+    [JellyfinMigration("2025-04-20T05:00:00", nameof(DisableTranscodingThrottling), "4124C2CD-E939-4FFB-9BE9-9B311C413638")]
+#pragma warning disable CS0618 // Type or member is obsolete
     internal class DisableTranscodingThrottling : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
     {
         private readonly ILogger<DisableTranscodingThrottling> _logger;
         private readonly IConfigurationManager _configManager;
@@ -18,15 +21,6 @@ namespace Jellyfin.Server.Migrations.Routines
             _configManager = configManager;
         }
 
-        /// <inheritdoc/>
-        public Guid Id => Guid.Parse("{4124C2CD-E939-4FFB-9BE9-9B311C413638}");
-
-        /// <inheritdoc/>
-        public string Name => "DisableTranscodingThrottling";
-
-        /// <inheritdoc/>
-        public bool PerformOnNewInstall => false;
-
         /// <inheritdoc/>
         public void Perform()
         {

+ 3 - 9
Jellyfin.Server/Migrations/Routines/FixAudioData.cs

@@ -16,7 +16,10 @@ namespace Jellyfin.Server.Migrations.Routines
     /// <summary>
     /// Fixes the data column of audio types to be deserializable.
     /// </summary>
+    [JellyfinMigration("2025-04-20T18:00:00", nameof(FixAudioData), "CF6FABC2-9FBE-4933-84A5-FFE52EF22A58")]
+#pragma warning disable CS0618 // Type or member is obsolete
     internal class FixAudioData : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
     {
         private const string DbFilename = "library.db";
         private readonly ILogger<FixAudioData> _logger;
@@ -33,15 +36,6 @@ namespace Jellyfin.Server.Migrations.Routines
             _logger = loggerFactory.CreateLogger<FixAudioData>();
         }
 
-        /// <inheritdoc/>
-        public Guid Id => Guid.Parse("{CF6FABC2-9FBE-4933-84A5-FFE52EF22A58}");
-
-        /// <inheritdoc/>
-        public string Name => "FixAudioData";
-
-        /// <inheritdoc/>
-        public bool PerformOnNewInstall => false;
-
         /// <inheritdoc/>
         public void Perform()
         {

+ 3 - 9
Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs

@@ -13,7 +13,10 @@ namespace Jellyfin.Server.Migrations.Routines;
 /// <summary>
 /// Properly set playlist owner.
 /// </summary>
+[JellyfinMigration("2025-04-20T15:00:00", nameof(FixPlaylistOwner), "615DFA9E-2497-4DBB-A472-61938B752C5B")]
+#pragma warning disable CS0618 // Type or member is obsolete
 internal class FixPlaylistOwner : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
 {
     private readonly ILogger<FixPlaylistOwner> _logger;
     private readonly ILibraryManager _libraryManager;
@@ -29,15 +32,6 @@ internal class FixPlaylistOwner : IMigrationRoutine
         _playlistManager = playlistManager;
     }
 
-    /// <inheritdoc/>
-    public Guid Id => Guid.Parse("{615DFA9E-2497-4DBB-A472-61938B752C5B}");
-
-    /// <inheritdoc/>
-    public string Name => "FixPlaylistOwner";
-
-    /// <inheritdoc/>
-    public bool PerformOnNewInstall => false;
-
     /// <inheritdoc/>
     public void Perform()
     {

+ 3 - 9
Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs

@@ -14,7 +14,10 @@ namespace Jellyfin.Server.Migrations.Routines
     /// <summary>
     /// The migration routine for migrating the activity log database to EF Core.
     /// </summary>
+    [JellyfinMigration("2025-04-20T07:00:00", nameof(MigrateActivityLogDb), "3793eb59-bc8c-456c-8b9f-bd5a62a42978")]
+#pragma warning disable CS0618 // Type or member is obsolete
     public class MigrateActivityLogDb : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
     {
         private const string DbFilename = "activitylog.db";
 
@@ -35,15 +38,6 @@ namespace Jellyfin.Server.Migrations.Routines
             _paths = paths;
         }
 
-        /// <inheritdoc/>
-        public Guid Id => Guid.Parse("3793eb59-bc8c-456c-8b9f-bd5a62a42978");
-
-        /// <inheritdoc/>
-        public string Name => "MigrateActivityLogDatabase";
-
-        /// <inheritdoc/>
-        public bool PerformOnNewInstall => false;
-
         /// <inheritdoc/>
         public void Perform()
         {

+ 3 - 9
Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs

@@ -15,7 +15,10 @@ namespace Jellyfin.Server.Migrations.Routines
     /// <summary>
     /// A migration that moves data from the authentication database into the new schema.
     /// </summary>
+    [JellyfinMigration("2025-04-20T14:00:00", nameof(MigrateAuthenticationDb), "5BD72F41-E6F3-4F60-90AA-09869ABE0E22")]
+#pragma warning disable CS0618 // Type or member is obsolete
     public class MigrateAuthenticationDb : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
     {
         private const string DbFilename = "authentication.db";
 
@@ -43,15 +46,6 @@ namespace Jellyfin.Server.Migrations.Routines
             _userManager = userManager;
         }
 
-        /// <inheritdoc />
-        public Guid Id => Guid.Parse("5BD72F41-E6F3-4F60-90AA-09869ABE0E22");
-
-        /// <inheritdoc />
-        public string Name => "MigrateAuthenticationDatabase";
-
-        /// <inheritdoc />
-        public bool PerformOnNewInstall => false;
-
         /// <inheritdoc />
         public void Perform()
         {

+ 3 - 9
Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs

@@ -20,7 +20,10 @@ namespace Jellyfin.Server.Migrations.Routines
     /// <summary>
     /// The migration routine for migrating the display preferences database to EF Core.
     /// </summary>
+    [JellyfinMigration("2025-04-20T12:00:00", nameof(MigrateDisplayPreferencesDb), "06387815-C3CC-421F-A888-FB5F9992BEA8")]
+#pragma warning disable CS0618 // Type or member is obsolete
     public class MigrateDisplayPreferencesDb : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
     {
         private const string DbFilename = "displaypreferences.db";
 
@@ -51,15 +54,6 @@ namespace Jellyfin.Server.Migrations.Routines
             _jsonOptions.Converters.Add(new JsonStringEnumConverter());
         }
 
-        /// <inheritdoc />
-        public Guid Id => Guid.Parse("06387815-C3CC-421F-A888-FB5F9992BEA8");
-
-        /// <inheritdoc />
-        public string Name => "MigrateDisplayPreferencesDatabase";
-
-        /// <inheritdoc />
-        public bool PerformOnNewInstall => false;
-
         /// <inheritdoc />
         public void Perform()
         {

+ 1 - 9
Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs

@@ -19,6 +19,7 @@ namespace Jellyfin.Server.Migrations.Routines;
 /// <summary>
 /// Migration to move extracted files to the new directories.
 /// </summary>
+[JellyfinMigration("2025-04-21T00:00:00", nameof(MigrateKeyframeData), "EA4bCAE1-09A4-428E-9B90-4B4FD2EA1B24")]
 public class MigrateKeyframeData : IDatabaseMigrationRoutine
 {
     private readonly ILogger<MigrateKeyframeData> _logger;
@@ -44,15 +45,6 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine
 
     private string KeyframeCachePath => Path.Combine(_appPaths.DataPath, "keyframes");
 
-    /// <inheritdoc />
-    public Guid Id => new("EA4bCAE1-09A4-428E-9B90-4B4FD2EA1B24");
-
-    /// <inheritdoc />
-    public string Name => "MigrateKeyframeData";
-
-    /// <inheritdoc />
-    public bool PerformOnNewInstall => false;
-
     /// <inheritdoc />
     public void Perform()
     {

+ 19 - 12
Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs

@@ -16,10 +16,19 @@ using Jellyfin.Database.Implementations.Entities;
 using Jellyfin.Extensions;
 using Jellyfin.Server.Implementations.Item;
 using MediaBrowser.Controller;
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Chapters;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.IO;
 using Microsoft.Data.Sqlite;
 using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity;
 using Chapter = Jellyfin.Database.Implementations.Entities.Chapter;
@@ -29,6 +38,7 @@ namespace Jellyfin.Server.Migrations.Routines;
 /// <summary>
 /// The migration routine for migrating the userdata database to EF Core.
 /// </summary>
+[JellyfinMigration("2025-04-20T20:00:00", nameof(MigrateLibraryDb), "36445464-849f-429f-9ad0-bb130efa0664")]
 internal class MigrateLibraryDb : IDatabaseMigrationRoutine
 {
     private const string DbFilename = "library.db";
@@ -45,11 +55,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
     /// <param name="provider">The database provider.</param>
     /// <param name="paths">The server application paths.</param>
     /// <param name="jellyfinDatabaseProvider">The database provider for special access.</param>
+    /// <param name="serviceProvider">The Service provider.</param>
     public MigrateLibraryDb(
         ILogger<MigrateLibraryDb> logger,
         IDbContextFactory<JellyfinDbContext> provider,
         IServerApplicationPaths paths,
-        IJellyfinDatabaseProvider jellyfinDatabaseProvider)
+        IJellyfinDatabaseProvider jellyfinDatabaseProvider,
+        IServiceProvider serviceProvider)
     {
         _logger = logger;
         _provider = provider;
@@ -57,15 +69,6 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
         _jellyfinDatabaseProvider = jellyfinDatabaseProvider;
     }
 
-    /// <inheritdoc/>
-    public Guid Id => Guid.Parse("36445464-849f-429f-9ad0-bb130efa0664");
-
-    /// <inheritdoc/>
-    public string Name => "MigrateLibraryDbData";
-
-    /// <inheritdoc/>
-    public bool PerformOnNewInstall => false; // TODO Change back after testing
-
     /// <inheritdoc/>
     public void Perform()
     {
@@ -73,6 +76,12 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
 
         var dataPath = _paths.DataPath;
         var libraryDbPath = Path.Combine(dataPath, DbFilename);
+        if (!File.Exists(libraryDbPath))
+        {
+            _logger.LogError("Cannot migrate {LibraryDb} as it does not exist..", libraryDbPath);
+            return;
+        }
+
         using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly");
 
         var fullOperationTimer = new Stopwatch();
@@ -395,8 +404,6 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
 
         _logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old");
         File.Move(libraryDbPath, libraryDbPath + ".old", true);
-
-        _jellyfinDatabaseProvider.RunScheduledOptimisation(CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult();
     }
 
     private DatabaseMigrationStep GetPreparedDbContext(string operationName)

+ 1 - 9
Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs

@@ -10,6 +10,7 @@ namespace Jellyfin.Server.Migrations.Routines
     /// <summary>
     /// Migrate rating levels.
     /// </summary>
+    [JellyfinMigration("2025-04-20T22:00:00", nameof(MigrateRatingLevels), "98724538-EB11-40E3-931A-252C55BDDE7A")]
     internal class MigrateRatingLevels : IDatabaseMigrationRoutine
     {
         private readonly ILogger<MigrateRatingLevels> _logger;
@@ -26,15 +27,6 @@ namespace Jellyfin.Server.Migrations.Routines
             _logger = loggerFactory.CreateLogger<MigrateRatingLevels>();
         }
 
-        /// <inheritdoc/>
-        public Guid Id => Guid.Parse("{98724538-EB11-40E3-931A-252C55BDDE7A}");
-
-        /// <inheritdoc/>
-        public string Name => "MigrateRatingLevels";
-
-        /// <inheritdoc/>
-        public bool PerformOnNewInstall => false;
-
         /// <inheritdoc/>
         public void Perform()
         {

+ 3 - 9
Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs

@@ -22,7 +22,10 @@ namespace Jellyfin.Server.Migrations.Routines
     /// <summary>
     /// The migration routine for migrating the user database to EF Core.
     /// </summary>
+    [JellyfinMigration("2025-04-20T10:00:00", nameof(MigrateUserDb), "5C4B82A2-F053-4009-BD05-B6FCAD82F14C")]
+#pragma warning disable CS0618 // Type or member is obsolete
     public class MigrateUserDb : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
     {
         private const string DbFilename = "users.db";
 
@@ -50,15 +53,6 @@ namespace Jellyfin.Server.Migrations.Routines
             _xmlSerializer = xmlSerializer;
         }
 
-        /// <inheritdoc/>
-        public Guid Id => Guid.Parse("5C4B82A2-F053-4009-BD05-B6FCAD82F14C");
-
-        /// <inheritdoc/>
-        public string Name => "MigrateUserDatabase";
-
-        /// <inheritdoc/>
-        public bool PerformOnNewInstall => false;
-
         /// <inheritdoc/>
         public void Perform()
         {

+ 3 - 9
Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs

@@ -24,7 +24,10 @@ namespace Jellyfin.Server.Migrations.Routines;
 /// <summary>
 /// Migration to move extracted files to the new directories.
 /// </summary>
+[JellyfinMigration("2025-04-20T21:00:00", nameof(MoveExtractedFiles), "9063b0Ef-CFF1-4EDC-9A13-74093681A89B")]
+#pragma warning disable CS0618 // Type or member is obsolete
 public class MoveExtractedFiles : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
 {
     private readonly IApplicationPaths _appPaths;
     private readonly ILogger<MoveExtractedFiles> _logger;
@@ -58,15 +61,6 @@ public class MoveExtractedFiles : IMigrationRoutine
 
     private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments");
 
-    /// <inheritdoc />
-    public Guid Id => new("9063b0Ef-CFF1-4EDC-9A13-74093681A89B");
-
-    /// <inheritdoc />
-    public string Name => "MoveExtractedFiles";
-
-    /// <inheritdoc />
-    public bool PerformOnNewInstall => false;
-
     /// <inheritdoc />
     public void Perform()
     {

+ 4 - 10
Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs

@@ -15,7 +15,10 @@ namespace Jellyfin.Server.Migrations.Routines;
 /// <summary>
 /// Migration to move trickplay files to the new directory.
 /// </summary>
+[JellyfinMigration("2025-04-20T23:00:00", nameof(MoveTrickplayFiles), "9540D44A-D8DC-11EF-9CBB-B77274F77C52", RunMigrationOnSetup = true)]
+#pragma warning disable CS0618 // Type or member is obsolete
 public class MoveTrickplayFiles : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
 {
     private readonly ITrickplayManager _trickplayManager;
     private readonly IFileSystem _fileSystem;
@@ -41,15 +44,6 @@ public class MoveTrickplayFiles : IMigrationRoutine
         _logger = logger;
     }
 
-    /// <inheritdoc />
-    public Guid Id => new("9540D44A-D8DC-11EF-9CBB-B77274F77C52");
-
-    /// <inheritdoc />
-    public string Name => "MoveTrickplayFiles";
-
-    /// <inheritdoc />
-    public bool PerformOnNewInstall => true;
-
     /// <inheritdoc />
     public void Perform()
     {
@@ -103,7 +97,7 @@ public class MoveTrickplayFiles : IMigrationRoutine
             offset += Limit;
             previousCount = trickplayInfos.Count;
 
-            _logger.LogInformation("Checked: {Checked} - Moved: {Count} - Time: {Time}", itemCount, offset, sw.Elapsed);
+            _logger.LogInformation("Checked: {Checked} - Moved: {Count} - Time: {Time}", offset, itemCount, sw.Elapsed);
         } while (previousCount == Limit);
 
         _logger.LogInformation("Moved {Count} items in {Time}", itemCount, sw.Elapsed);

+ 3 - 9
Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs

@@ -7,7 +7,10 @@ namespace Jellyfin.Server.Migrations.Routines
     /// <summary>
     /// Migration to initialize system configuration with the default plugin repository.
     /// </summary>
+    [JellyfinMigration("2025-04-20T11:00:00", nameof(ReaddDefaultPluginRepository), "5F86E7F6-D966-4C77-849D-7A7B40B68C4E", RunMigrationOnSetup = true)]
+#pragma warning disable CS0618 // Type or member is obsolete
     public class ReaddDefaultPluginRepository : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
     {
         private readonly IServerConfigurationManager _serverConfigurationManager;
 
@@ -26,15 +29,6 @@ namespace Jellyfin.Server.Migrations.Routines
             _serverConfigurationManager = serverConfigurationManager;
         }
 
-        /// <inheritdoc/>
-        public Guid Id => Guid.Parse("5F86E7F6-D966-4C77-849D-7A7B40B68C4E");
-
-        /// <inheritdoc/>
-        public string Name => "ReaddDefaultPluginRepository";
-
-        /// <inheritdoc/>
-        public bool PerformOnNewInstall => true;
-
         /// <inheritdoc/>
         public void Perform()
         {

+ 3 - 9
Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs

@@ -8,7 +8,10 @@ namespace Jellyfin.Server.Migrations.Routines
     /// <summary>
     /// Removes the old 'RemoveDownloadImagesInAdvance' from library options.
     /// </summary>
+    [JellyfinMigration("2025-04-20T13:00:00", nameof(RemoveDownloadImagesInAdvance), "A81F75E0-8F43-416F-A5E8-516CCAB4D8CC")]
+#pragma warning disable CS0618 // Type or member is obsolete
     internal class RemoveDownloadImagesInAdvance : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
     {
         private readonly ILogger<RemoveDownloadImagesInAdvance> _logger;
         private readonly ILibraryManager _libraryManager;
@@ -19,15 +22,6 @@ namespace Jellyfin.Server.Migrations.Routines
             _libraryManager = libraryManager;
         }
 
-        /// <inheritdoc/>
-        public Guid Id => Guid.Parse("{A81F75E0-8F43-416F-A5E8-516CCAB4D8CC}");
-
-        /// <inheritdoc/>
-        public string Name => "RemoveDownloadImagesInAdvance";
-
-        /// <inheritdoc/>
-        public bool PerformOnNewInstall => false;
-
         /// <inheritdoc/>
         public void Perform()
         {

+ 3 - 9
Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs

@@ -12,7 +12,10 @@ namespace Jellyfin.Server.Migrations.Routines
     /// <summary>
     /// Remove duplicate entries which were caused by a bug where a file was considered to be an "Extra" to itself.
     /// </summary>
+    [JellyfinMigration("2025-04-20T08:00:00", nameof(RemoveDuplicateExtras), "ACBE17B7-8435-4A83-8B64-6FCF162CB9BD")]
+#pragma warning disable CS0618 // Type or member is obsolete
     internal class RemoveDuplicateExtras : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
     {
         private const string DbFilename = "library.db";
         private readonly ILogger<RemoveDuplicateExtras> _logger;
@@ -24,15 +27,6 @@ namespace Jellyfin.Server.Migrations.Routines
             _paths = paths;
         }
 
-        /// <inheritdoc/>
-        public Guid Id => Guid.Parse("{ACBE17B7-8435-4A83-8B64-6FCF162CB9BD}");
-
-        /// <inheritdoc/>
-        public string Name => "RemoveDuplicateExtras";
-
-        /// <inheritdoc/>
-        public bool PerformOnNewInstall => false;
-
         /// <inheritdoc/>
         public void Perform()
         {

+ 3 - 9
Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs

@@ -11,7 +11,10 @@ namespace Jellyfin.Server.Migrations.Routines;
 /// <summary>
 /// Remove duplicate playlist entries.
 /// </summary>
+[JellyfinMigration("2025-04-20T19:00:00", nameof(RemoveDuplicatePlaylistChildren), "96C156A2-7A13-4B3B-A8B8-FB80C94D20C0")]
+#pragma warning disable CS0618 // Type or member is obsolete
 internal class RemoveDuplicatePlaylistChildren : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
 {
     private readonly ILibraryManager _libraryManager;
     private readonly IPlaylistManager _playlistManager;
@@ -24,15 +27,6 @@ internal class RemoveDuplicatePlaylistChildren : IMigrationRoutine
         _playlistManager = playlistManager;
     }
 
-    /// <inheritdoc/>
-    public Guid Id => Guid.Parse("{96C156A2-7A13-4B3B-A8B8-FB80C94D20C0}");
-
-    /// <inheritdoc/>
-    public string Name => "RemoveDuplicatePlaylistChildren";
-
-    /// <inheritdoc/>
-    public bool PerformOnNewInstall => false;
-
     /// <inheritdoc/>
     public void Perform()
     {

+ 3 - 9
Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs

@@ -6,7 +6,10 @@ namespace Jellyfin.Server.Migrations.Routines;
 /// <summary>
 /// Migration to update the default Jellyfin plugin repository.
 /// </summary>
+[JellyfinMigration("2025-04-20T17:00:00", nameof(UpdateDefaultPluginRepository), "852816E0-2712-49A9-9240-C6FC5FCAD1A8", RunMigrationOnSetup = true)]
+#pragma warning disable CS0618 // Type or member is obsolete
 public class UpdateDefaultPluginRepository : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
 {
     private const string NewRepositoryUrl = "https://repo.jellyfin.org/files/plugin/manifest.json";
     private const string OldRepositoryUrl = "https://repo.jellyfin.org/releases/plugin/manifest-stable.json";
@@ -22,15 +25,6 @@ public class UpdateDefaultPluginRepository : IMigrationRoutine
         _serverConfigurationManager = serverConfigurationManager;
     }
 
-    /// <inheritdoc />
-    public Guid Id => new("852816E0-2712-49A9-9240-C6FC5FCAD1A8");
-
-    /// <inheritdoc />
-    public string Name => "UpdateDefaultPluginRepository10.9";
-
-    /// <inheritdoc />
-    public bool PerformOnNewInstall => true;
-
     /// <inheritdoc />
     public void Perform()
     {

+ 51 - 0
Jellyfin.Server/Migrations/Stages/CodeMigration.cs

@@ -0,0 +1,51 @@
+using System;
+using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Jellyfin.Server.Migrations.Stages;
+
+internal class CodeMigration(Type migrationType, JellyfinMigrationAttribute metadata)
+{
+    public Type MigrationType { get; } = migrationType;
+
+    public JellyfinMigrationAttribute Metadata { get; } = metadata;
+
+    public string BuildCodeMigrationId()
+    {
+        return Metadata.Order.ToString("yyyyMMddHHmmsss", CultureInfo.InvariantCulture) + "_" + MigrationType.Name!;
+    }
+
+    public async Task Perform(IServiceProvider? serviceProvider, CancellationToken cancellationToken)
+    {
+#pragma warning disable CS0618 // Type or member is obsolete
+        if (typeof(IMigrationRoutine).IsAssignableFrom(MigrationType))
+        {
+            if (serviceProvider is null)
+            {
+                ((IMigrationRoutine)Activator.CreateInstance(MigrationType)!).Perform();
+            }
+            else
+            {
+                ((IMigrationRoutine)ActivatorUtilities.CreateInstance(serviceProvider, MigrationType)).Perform();
+#pragma warning restore CS0618 // Type or member is obsolete
+            }
+        }
+        else if (typeof(IAsyncMigrationRoutine).IsAssignableFrom(MigrationType))
+        {
+            if (serviceProvider is null)
+            {
+                await ((IAsyncMigrationRoutine)Activator.CreateInstance(MigrationType)!).PerformAsync(cancellationToken).ConfigureAwait(false);
+            }
+            else
+            {
+                await ((IAsyncMigrationRoutine)ActivatorUtilities.CreateInstance(serviceProvider, MigrationType)).PerformAsync(cancellationToken).ConfigureAwait(false);
+            }
+        }
+        else
+        {
+            throw new InvalidOperationException($"The type {MigrationType} does not implement either IMigrationRoutine or IAsyncMigrationRoutine and is not a valid migration type");
+        }
+    }
+}

+ 26 - 0
Jellyfin.Server/Migrations/Stages/JellyfinMigrationStageTypes.cs

@@ -0,0 +1,26 @@
+namespace Jellyfin.Server.Migrations.Stages;
+
+/// <summary>
+/// Defines the stages the <see cref="JellyfinMigrationService"/> supports.
+/// </summary>
+#pragma warning disable CA1008 // Enums should have zero value
+public enum JellyfinMigrationStageTypes
+#pragma warning restore CA1008 // Enums should have zero value
+{
+    /// <summary>
+    /// Runs before services are initialised.
+    /// Reserved for migrations that are modifying the application server itself. Should be avoided if possible.
+    /// </summary>
+    PreInitialisation = 1,
+
+    /// <summary>
+    /// Runs after the host has been configured and includes the database migrations.
+    /// Allows the mix order of migrations that contain application code and database changes.
+    /// </summary>
+    CoreInitialisaition = 2,
+
+    /// <summary>
+    /// Runs after services has been registered and initialised. Last step before running the server.
+    /// </summary>
+    AppInitialisation = 3
+}

+ 16 - 0
Jellyfin.Server/Migrations/Stages/MigrationStage.cs

@@ -0,0 +1,16 @@
+using System.Collections.ObjectModel;
+
+namespace Jellyfin.Server.Migrations.Stages;
+
+/// <summary>
+/// Defines a Stage that can be Invoked and Handled at different times from the code.
+/// </summary>
+internal class MigrationStage : Collection<CodeMigration>
+{
+    public MigrationStage(JellyfinMigrationStageTypes stage)
+    {
+        Stage = stage;
+    }
+
+    public JellyfinMigrationStageTypes Stage { get; }
+}

+ 49 - 4
Jellyfin.Server/Program.cs

@@ -9,17 +9,20 @@ using System.Threading;
 using System.Threading.Tasks;
 using CommandLine;
 using Emby.Server.Implementations;
+using Emby.Server.Implementations.Configuration;
+using Emby.Server.Implementations.Serialization;
 using Jellyfin.Database.Implementations;
 using Jellyfin.Server.Extensions;
 using Jellyfin.Server.Helpers;
+using Jellyfin.Server.Implementations.DatabaseConfiguration;
+using Jellyfin.Server.Implementations.Extensions;
 using Jellyfin.Server.Implementations.StorageHelpers;
+using Jellyfin.Server.Migrations;
 using Jellyfin.Server.ServerSetupApp;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using Microsoft.AspNetCore.Hosting;
-using Microsoft.Data.Sqlite;
-using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
@@ -126,7 +129,8 @@ namespace Jellyfin.Server
             StorageHelper.TestCommonPathsForStorageCapacity(appPaths, _loggerFactory.CreateLogger<Startup>());
 
             StartupHelpers.PerformStaticInitialization();
-            await Migrations.MigrationRunner.RunPreStartup(appPaths, _loggerFactory).ConfigureAwait(false);
+
+            await ApplyStartupMigrationAsync(appPaths, startupConfig).ConfigureAwait(false);
 
             do
             {
@@ -171,9 +175,11 @@ namespace Jellyfin.Server
 
                 // Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection.
                 appHost.ServiceProvider = _jellyfinHost.Services;
+                await ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.CoreInitialisaition).ConfigureAwait(false);
 
                 await appHost.InitializeServices(startupConfig).ConfigureAwait(false);
-                await Migrations.MigrationRunner.Run(appHost, _loggerFactory).ConfigureAwait(false);
+
+                await ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.AppInitialisation).ConfigureAwait(false);
 
                 try
                 {
@@ -223,6 +229,45 @@ namespace Jellyfin.Server
             }
         }
 
+        /// <summary>
+        /// [Internal]Runs the startup Migrations.
+        /// </summary>
+        /// <remarks>
+        /// Not intended to be used other then by jellyfin and its tests.
+        /// </remarks>
+        /// <param name="appPaths">Application Paths.</param>
+        /// <param name="startupConfig">Startup Config.</param>
+        /// <returns>A task.</returns>
+        public static async Task ApplyStartupMigrationAsync(ServerApplicationPaths appPaths, IConfiguration startupConfig)
+        {
+            var startupConfigurationManager = new ServerConfigurationManager(appPaths, _loggerFactory, new MyXmlSerializer());
+            startupConfigurationManager.AddParts([new DatabaseConfigurationFactory()]);
+            var migrationStartupServiceProvider = new ServiceCollection()
+                .AddLogging(d => d.AddSerilog())
+                .AddJellyfinDbContext(startupConfigurationManager, startupConfig)
+                .AddSingleton<IApplicationPaths>(appPaths)
+                .AddSingleton<ServerApplicationPaths>(appPaths);
+            var startupService = migrationStartupServiceProvider.BuildServiceProvider();
+            var jellyfinMigrationService = ActivatorUtilities.CreateInstance<JellyfinMigrationService>(startupService);
+            await jellyfinMigrationService.CheckFirstTimeRunOrMigration(appPaths).ConfigureAwait(false);
+            await jellyfinMigrationService.MigrateStepAsync(Migrations.Stages.JellyfinMigrationStageTypes.PreInitialisation, startupService).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// [Internal]Runs the Jellyfin migrator service with the Core stage.
+        /// </summary>
+        /// <remarks>
+        /// Not intended to be used other then by jellyfin and its tests.
+        /// </remarks>
+        /// <param name="serviceProvider">The service provider.</param>
+        /// <param name="jellyfinMigrationStage">The stage to run.</param>
+        /// <returns>A task.</returns>
+        public static async Task ApplyCoreMigrationsAsync(IServiceProvider serviceProvider, Migrations.Stages.JellyfinMigrationStageTypes jellyfinMigrationStage)
+        {
+            var jellyfinMigrationService = ActivatorUtilities.CreateInstance<JellyfinMigrationService>(serviceProvider);
+            await jellyfinMigrationService.MigrateStepAsync(jellyfinMigrationStage, serviceProvider).ConfigureAwait(false);
+        }
+
         /// <summary>
         /// Create the application configuration.
         /// </summary>

+ 5 - 0
src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs

@@ -76,6 +76,11 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider
     /// <inheritdoc/>
     public async Task RunShutdownTask(CancellationToken cancellationToken)
     {
+        if (DbContextFactory is null)
+        {
+            return;
+        }
+
         // Run before disposing the application
         var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
         await using (context.ConfigureAwait(false))

+ 5 - 0
tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs

@@ -6,6 +6,7 @@ using Emby.Server.Implementations;
 using Jellyfin.Server.Extensions;
 using Jellyfin.Server.Helpers;
 using MediaBrowser.Common;
+using MediaBrowser.Common.Configuration;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Mvc.Testing;
 using Microsoft.Extensions.Configuration;
@@ -103,7 +104,11 @@ namespace Jellyfin.Server.Integration.Tests
             var host = builder.Build();
             var appHost = (TestAppHost)host.Services.GetRequiredService<IApplicationHost>();
             appHost.ServiceProvider = host.Services;
+            var applicationPaths = appHost.ServiceProvider.GetRequiredService<IApplicationPaths>();
+            Program.ApplyStartupMigrationAsync((ServerApplicationPaths)applicationPaths, appHost.ServiceProvider.GetRequiredService<IConfiguration>()).GetAwaiter().GetResult();
+            Program.ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.CoreInitialisaition).GetAwaiter().GetResult();
             appHost.InitializeServices(Mock.Of<IConfiguration>()).GetAwaiter().GetResult();
+            Program.ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.AppInitialisation).GetAwaiter().GetResult();
             host.Start();
 
             appHost.RunStartupTasksAsync().GetAwaiter().GetResult();