diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 7b07243da7..fa6e9ff977 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -580,21 +580,6 @@ namespace Emby.Server.Implementations
/// A task representing the service initialization operation.
public async Task InitializeServices(IConfiguration startupConfig)
{
- var factory = Resolve>();
- var provider = Resolve();
- 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();
await localizationManager.LoadAll().ConfigureAwait(false);
diff --git a/Jellyfin.Server/Migrations/IAsyncMigrationRoutine.cs b/Jellyfin.Server/Migrations/IAsyncMigrationRoutine.cs
new file mode 100644
index 0000000000..5b6a5fe942
--- /dev/null
+++ b/Jellyfin.Server/Migrations/IAsyncMigrationRoutine.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Jellyfin.Server.Migrations;
+
+///
+/// Interface that describes a migration routine.
+///
+internal interface IAsyncMigrationRoutine
+{
+ ///
+ /// Execute the migration routine.
+ ///
+ /// A cancellation token triggered if the migration should be aborted.
+ /// A representing the asynchronous operation.
+ public Task PerformAsync(CancellationToken cancellationToken);
+}
+
+///
+/// Interface that describes a migration routine.
+///
+[Obsolete("Use IAsyncMigrationRoutine instead")]
+internal interface IMigrationRoutine
+{
+ ///
+ /// Execute the migration routine.
+ ///
+ [Obsolete("Use IAsyncMigrationRoutine.PerformAsync instead")]
+ public void Perform();
+}
diff --git a/Jellyfin.Server/Migrations/IDatabaseMigrationRoutine.cs b/Jellyfin.Server/Migrations/IDatabaseMigrationRoutine.cs
index 78ff1e3fd0..d2d80a81eb 100644
--- a/Jellyfin.Server/Migrations/IDatabaseMigrationRoutine.cs
+++ b/Jellyfin.Server/Migrations/IDatabaseMigrationRoutine.cs
@@ -7,6 +7,8 @@ namespace Jellyfin.Server.Migrations;
///
/// Defines a migration that operates on the Database.
///
+#pragma warning disable CS0618 // Type or member is obsolete
internal interface IDatabaseMigrationRoutine : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
{
}
diff --git a/Jellyfin.Server/Migrations/IMigrationRoutine.cs b/Jellyfin.Server/Migrations/IMigrationRoutine.cs
deleted file mode 100644
index 29f681df52..0000000000
--- a/Jellyfin.Server/Migrations/IMigrationRoutine.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using System;
-using Jellyfin.Server.Implementations;
-using Microsoft.EntityFrameworkCore.Internal;
-
-namespace Jellyfin.Server.Migrations
-{
- ///
- /// Interface that describes a migration routine.
- ///
- internal interface IMigrationRoutine
- {
- ///
- /// Gets the unique id for this migration. This should never be modified after the migration has been created.
- ///
- public Guid Id { get; }
-
- ///
- /// Gets the display name of the migration.
- ///
- public string Name { get; }
-
- ///
- /// Gets a value indicating whether to perform migration on a new install.
- ///
- public bool PerformOnNewInstall { get; }
-
- ///
- /// Execute the migration routine.
- ///
- public void Perform();
- }
-}
diff --git a/Jellyfin.Server/Migrations/JellyfinMigrationAttribute.cs b/Jellyfin.Server/Migrations/JellyfinMigrationAttribute.cs
new file mode 100644
index 0000000000..f523bc76c1
--- /dev/null
+++ b/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;
+
+///
+/// Declares an class as an migration with its set metadata.
+///
+[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
+public sealed class JellyfinMigrationAttribute : Attribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The ordering this migration should be applied to. Must be a valid DateTime ISO8601 formatted string.
+ /// The name of this Migration.
+ public JellyfinMigrationAttribute(string order, string name) : this(order, name, null)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class for legacy migrations.
+ ///
+ /// The ordering this migration should be applied to. Must be a valid DateTime ISO8601 formatted string.
+ /// The name of this Migration.
+ /// [ONLY FOR LEGACY MIGRATIONS]The unique key of this migration. Must be a valid Guid formatted string.
+ 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);
+ }
+ }
+
+ ///
+ /// Gets or Sets a value indicating whether the annoated migration should be executed on a fresh install.
+ ///
+ public bool RunMigrationOnSetup { get; set; }
+
+ ///
+ /// Gets or Sets the stage the annoated migration should be executed at. Defaults to .
+ ///
+ public JellyfinMigrationStageTypes Stage { get; set; } = JellyfinMigrationStageTypes.CoreInitialisaition;
+
+ ///
+ /// Gets the ordering of the migration.
+ ///
+ public DateTime Order { get; }
+
+ ///
+ /// Gets the name of the migration.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Gets the Legacy Key of the migration. Not required for new Migrations.
+ ///
+ public Guid? Key { get; }
+}
diff --git a/Jellyfin.Server/Migrations/JellyfinMigrationService.cs b/Jellyfin.Server/Migrations/JellyfinMigrationService.cs
new file mode 100644
index 0000000000..46c22d16cc
--- /dev/null
+++ b/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;
+
+///
+/// Handles Migration of the Jellyfin data structure.
+///
+internal class JellyfinMigrationService
+{
+ private readonly IDbContextFactory _dbContextFactory;
+ private readonly ILoggerFactory _loggerFactory;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Provides access to the jellyfin database.
+ /// The logger factory.
+ public JellyfinMigrationService(IDbContextFactory 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()))
+ .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 Migrations { get; set; }
+
+ public async Task CheckFirstTimeRunOrMigration(IApplicationPaths appPaths)
+ {
+ var logger = _loggerFactory.CreateLogger();
+ 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();
+
+ 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();
+ 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();
+ logger.LogInformation("Migrate stage {Stage}.", stage);
+ ICollection migrationStage = (Migrations.FirstOrDefault(e => e.Stage == stage) as ICollection) ?? [];
+
+ var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ var historyRepository = dbContext.GetService();
+ var migrationsAssembly = dbContext.GetService();
+ 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();
+ 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 _databaseMigrationInfo;
+
+ public InternalDatabaseMigration(KeyValuePair databaseMigrationInfo, JellyfinDbContext jellyfinDbContext)
+ {
+ _databaseMigrationInfo = databaseMigrationInfo;
+ _jellyfinDbContext = jellyfinDbContext;
+ }
+
+ public async Task PerformAsync(ILogger logger)
+ {
+ var migrator = _jellyfinDbContext.GetService();
+ await migrator.MigrateAsync(_databaseMigrationInfo.Key).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
deleted file mode 100644
index c223576dad..0000000000
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ /dev/null
@@ -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
-{
- ///
- /// The class that knows which migrations to apply and how to apply them.
- ///
- public sealed class MigrationRunner
- {
- ///
- /// The list of known pre-startup migrations, in order of applicability.
- ///
- private static readonly Type[] _preStartupMigrationTypes =
- {
- typeof(PreStartupRoutines.CreateNetworkConfiguration),
- typeof(PreStartupRoutines.MigrateMusicBrainzTimeout),
- typeof(PreStartupRoutines.MigrateNetworkConfiguration),
- typeof(PreStartupRoutines.MigrateEncodingOptions),
- typeof(PreStartupRoutines.RenameEnableGroupingIntoCollections)
- };
-
- ///
- /// The list of known migrations, in order of applicability.
- ///
- 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),
- };
-
- ///
- /// Run all needed migrations.
- ///
- /// CoreAppHost that hosts current version.
- /// Factory for making the logger.
- /// A representing the asynchronous operation.
- public static async Task Run(CoreAppHost host, ILoggerFactory loggerFactory)
- {
- var logger = loggerFactory.CreateLogger();
- var migrations = _migrationTypes
- .Select(m => ActivatorUtilities.CreateInstance(host.ServiceProvider, m))
- .OfType()
- .ToArray();
-
- var migrationOptions = host.ConfigurationManager.GetConfiguration(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())
- .ConfigureAwait(false);
- }
-
- ///
- /// Run all needed pre-startup migrations.
- ///
- /// Application paths.
- /// Factory for making the logger.
- /// A representing the asynchronous operation.
- public static async Task RunPreStartup(ServerApplicationPaths appPaths, ILoggerFactory loggerFactory)
- {
- var logger = loggerFactory.CreateLogger();
- var migrations = _preStartupMigrationTypes
- .Select(m => Activator.CreateInstance(m, appPaths, loggerFactory))
- .OfType()
- .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 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 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 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);
- }
- }
- }
-}
diff --git a/Jellyfin.Server/Migrations/MigrationsFactory.cs b/Jellyfin.Server/Migrations/MigrationsFactory.cs
deleted file mode 100644
index 23c1b1ee6f..0000000000
--- a/Jellyfin.Server/Migrations/MigrationsFactory.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using System.Collections.Generic;
-using MediaBrowser.Common.Configuration;
-
-namespace Jellyfin.Server.Migrations
-{
- ///
- /// A factory that can find a persistent file of the migration configuration, which lists all applied migrations.
- ///
- public class MigrationsFactory : IConfigurationFactory
- {
- ///
- public IEnumerable GetConfigurations()
- {
- return new[]
- {
- new MigrationsListStore()
- };
- }
- }
-}
diff --git a/Jellyfin.Server/Migrations/MigrationsListStore.cs b/Jellyfin.Server/Migrations/MigrationsListStore.cs
deleted file mode 100644
index 7a1ca66714..0000000000
--- a/Jellyfin.Server/Migrations/MigrationsListStore.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using MediaBrowser.Common.Configuration;
-
-namespace Jellyfin.Server.Migrations
-{
- ///
- /// A configuration that lists all the migration routines that were applied.
- ///
- public class MigrationsListStore : ConfigurationStore
- {
- ///
- /// The name of the configuration in the storage.
- ///
- public static readonly string StoreKey = "migrations";
-
- ///
- /// Initializes a new instance of the class.
- ///
- public MigrationsListStore()
- {
- ConfigurationType = typeof(MigrationOptions);
- Key = StoreKey;
- }
- }
-}
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs
index 8462d0a8c9..a62523b88f 100644
--- a/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs
@@ -8,7 +8,10 @@ using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.PreStartupRoutines;
///
+[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 _logger;
@@ -24,15 +27,6 @@ public class CreateNetworkConfiguration : IMigrationRoutine
_logger = loggerFactory.CreateLogger();
}
- ///
- public Guid Id => Guid.Parse("9B354818-94D5-4B68-AC49-E35CB85F9D84");
-
- ///
- public string Name => nameof(CreateNetworkConfiguration);
-
- ///
- public bool PerformOnNewInstall => false;
-
///
public void Perform()
{
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs
index 61f5620dc0..3455696994 100644
--- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs
@@ -10,7 +10,10 @@ using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.PreStartupRoutines;
///
+[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 _logger;
@@ -26,15 +29,6 @@ public class MigrateEncodingOptions : IMigrationRoutine
_logger = loggerFactory.CreateLogger();
}
- ///
- public Guid Id => Guid.Parse("A8E61960-7726-4450-8F3D-82C12DAABBCB");
-
- ///
- public string Name => nameof(MigrateEncodingOptions);
-
- ///
- public bool PerformOnNewInstall => false;
-
///
public void Perform()
{
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs
index 580282a5f5..bdbf0c1ce4 100644
--- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs
@@ -9,7 +9,10 @@ using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.PreStartupRoutines;
///
+[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 _logger;
@@ -25,15 +28,6 @@ public class MigrateMusicBrainzTimeout : IMigrationRoutine
_logger = loggerFactory.CreateLogger();
}
- ///
- public Guid Id => Guid.Parse("A6DCACF4-C057-4Ef9-80D3-61CEF9DDB4F0");
-
- ///
- public string Name => nameof(MigrateMusicBrainzTimeout);
-
- ///
- public bool PerformOnNewInstall => false;
-
///
public void Perform()
{
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs
index 09b2921714..f2790c1a1f 100644
--- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs
@@ -11,6 +11,7 @@ using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.PreStartupRoutines;
///
+[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();
}
- ///
- public Guid Id => Guid.Parse("4FB5C950-1991-11EE-9B4B-0800200C9A66");
-
- ///
- public string Name => nameof(MigrateNetworkConfiguration);
-
- ///
- public bool PerformOnNewInstall => false;
-
///
public void Perform()
{
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs
index 0a37b35a6a..c0ca7896fb 100644
--- a/Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs
@@ -9,7 +9,10 @@ using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.PreStartupRoutines;
///
+[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 _logger;
@@ -25,15 +28,6 @@ public class RenameEnableGroupingIntoCollections : IMigrationRoutine
_logger = loggerFactory.CreateLogger();
}
- ///
- public Guid Id => Guid.Parse("E73B777D-CD5C-4E71-957A-B86B3660B7CF");
-
- ///
- public string Name => nameof(RenameEnableGroupingIntoCollections);
-
- ///
- public bool PerformOnNewInstall => false;
-
///
public void Perform()
{
diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs b/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs
index 2047ec743e..7e92433423 100644
--- a/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs
+++ b/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs
@@ -7,7 +7,10 @@ namespace Jellyfin.Server.Migrations.Routines;
///
/// Migration to add the default cast receivers to the system config.
///
+[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;
}
- ///
- public Guid Id => new("34A1A1C4-5572-418E-A2F8-32CDFE2668E8");
-
- ///
- public string Name => "AddDefaultCastReceivers";
-
- ///
- public bool PerformOnNewInstall => true;
-
///
public void Perform()
{
diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs
index fc6b5d5979..603e01c180 100644
--- a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs
+++ b/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs
@@ -7,7 +7,10 @@ namespace Jellyfin.Server.Migrations.Routines
///
/// Migration to initialize system configuration with the default plugin repository.
///
+ [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;
}
- ///
- public Guid Id => Guid.Parse("EB58EBEE-9514-4B9B-8225-12E1A40020DF");
-
- ///
- public string Name => "AddDefaultPluginRepository";
-
- ///
- public bool PerformOnNewInstall => true;
-
///
public void Perform()
{
diff --git a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
index 5a8ef2e1cd..9d2a901cd9 100644
--- a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
+++ b/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.
///
+ [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
{
///
/// 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;
}
- ///
- public Guid Id => Guid.Parse("{EF103419-8451-40D8-9F34-D1A8E93A1679}");
-
- ///
- public string Name => "CreateLoggingConfigHierarchy";
-
- ///
- public bool PerformOnNewInstall => false;
-
///
public void Perform()
{
diff --git a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
index 378e88e25b..ca9bf32648 100644
--- a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
+++ b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
@@ -7,7 +7,10 @@ namespace Jellyfin.Server.Migrations.Routines
///
/// Disable transcode throttling for all installations since it is currently broken for certain video formats.
///
+ [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 _logger;
private readonly IConfigurationManager _configManager;
@@ -18,15 +21,6 @@ namespace Jellyfin.Server.Migrations.Routines
_configManager = configManager;
}
- ///
- public Guid Id => Guid.Parse("{4124C2CD-E939-4FFB-9BE9-9B311C413638}");
-
- ///
- public string Name => "DisableTranscodingThrottling";
-
- ///
- public bool PerformOnNewInstall => false;
-
///
public void Perform()
{
diff --git a/Jellyfin.Server/Migrations/Routines/FixAudioData.cs b/Jellyfin.Server/Migrations/Routines/FixAudioData.cs
index a202533692..6ebb5000e4 100644
--- a/Jellyfin.Server/Migrations/Routines/FixAudioData.cs
+++ b/Jellyfin.Server/Migrations/Routines/FixAudioData.cs
@@ -16,7 +16,10 @@ namespace Jellyfin.Server.Migrations.Routines
///
/// Fixes the data column of audio types to be deserializable.
///
+ [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 _logger;
@@ -33,15 +36,6 @@ namespace Jellyfin.Server.Migrations.Routines
_logger = loggerFactory.CreateLogger();
}
- ///
- public Guid Id => Guid.Parse("{CF6FABC2-9FBE-4933-84A5-FFE52EF22A58}");
-
- ///
- public string Name => "FixAudioData";
-
- ///
- public bool PerformOnNewInstall => false;
-
///
public void Perform()
{
diff --git a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
index 192c170b26..f31c1afbd3 100644
--- a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
+++ b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
@@ -13,7 +13,10 @@ namespace Jellyfin.Server.Migrations.Routines;
///
/// Properly set playlist owner.
///
+[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 _logger;
private readonly ILibraryManager _libraryManager;
@@ -29,15 +32,6 @@ internal class FixPlaylistOwner : IMigrationRoutine
_playlistManager = playlistManager;
}
- ///
- public Guid Id => Guid.Parse("{615DFA9E-2497-4DBB-A472-61938B752C5B}");
-
- ///
- public string Name => "FixPlaylistOwner";
-
- ///
- public bool PerformOnNewInstall => false;
-
///
public void Perform()
{
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
index e9fe9abceb..14089cac75 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
@@ -14,7 +14,10 @@ namespace Jellyfin.Server.Migrations.Routines
///
/// The migration routine for migrating the activity log database to EF Core.
///
+ [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;
}
- ///
- public Guid Id => Guid.Parse("3793eb59-bc8c-456c-8b9f-bd5a62a42978");
-
- ///
- public string Name => "MigrateActivityLogDatabase";
-
- ///
- public bool PerformOnNewInstall => false;
-
///
public void Perform()
{
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
index feaf46c843..e4362f44da 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
@@ -15,7 +15,10 @@ namespace Jellyfin.Server.Migrations.Routines
///
/// A migration that moves data from the authentication database into the new schema.
///
+ [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;
}
- ///
- public Guid Id => Guid.Parse("5BD72F41-E6F3-4F60-90AA-09869ABE0E22");
-
- ///
- public string Name => "MigrateAuthenticationDatabase";
-
- ///
- public bool PerformOnNewInstall => false;
-
///
public void Perform()
{
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index a8fa2e52a1..49ed01d6bb 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -20,7 +20,10 @@ namespace Jellyfin.Server.Migrations.Routines
///
/// The migration routine for migrating the display preferences database to EF Core.
///
+ [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());
}
- ///
- public Guid Id => Guid.Parse("06387815-C3CC-421F-A888-FB5F9992BEA8");
-
- ///
- public string Name => "MigrateDisplayPreferencesDatabase";
-
- ///
- public bool PerformOnNewInstall => false;
-
///
public void Perform()
{
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs
index 68d7a7b876..c5bc702789 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs
@@ -19,6 +19,7 @@ namespace Jellyfin.Server.Migrations.Routines;
///
/// Migration to move extracted files to the new directories.
///
+[JellyfinMigration("2025-04-21T00:00:00", nameof(MigrateKeyframeData), "EA4bCAE1-09A4-428E-9B90-4B4FD2EA1B24")]
public class MigrateKeyframeData : IDatabaseMigrationRoutine
{
private readonly ILogger _logger;
@@ -44,15 +45,6 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine
private string KeyframeCachePath => Path.Combine(_appPaths.DataPath, "keyframes");
- ///
- public Guid Id => new("EA4bCAE1-09A4-428E-9B90-4B4FD2EA1B24");
-
- ///
- public string Name => "MigrateKeyframeData";
-
- ///
- public bool PerformOnNewInstall => false;
-
///
public void Perform()
{
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
index 105fd555f6..8374508e66 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
+++ b/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;
///
/// The migration routine for migrating the userdata database to EF Core.
///
+[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
/// The database provider.
/// The server application paths.
/// The database provider for special access.
+ /// The Service provider.
public MigrateLibraryDb(
ILogger logger,
IDbContextFactory provider,
IServerApplicationPaths paths,
- IJellyfinDatabaseProvider jellyfinDatabaseProvider)
+ IJellyfinDatabaseProvider jellyfinDatabaseProvider,
+ IServiceProvider serviceProvider)
{
_logger = logger;
_provider = provider;
@@ -57,15 +69,6 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
_jellyfinDatabaseProvider = jellyfinDatabaseProvider;
}
- ///
- public Guid Id => Guid.Parse("36445464-849f-429f-9ad0-bb130efa0664");
-
- ///
- public string Name => "MigrateLibraryDbData";
-
- ///
- public bool PerformOnNewInstall => false; // TODO Change back after testing
-
///
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)
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
index c38beb7232..96276e9b10 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
@@ -10,6 +10,7 @@ namespace Jellyfin.Server.Migrations.Routines
///
/// Migrate rating levels.
///
+ [JellyfinMigration("2025-04-20T22:00:00", nameof(MigrateRatingLevels), "98724538-EB11-40E3-931A-252C55BDDE7A")]
internal class MigrateRatingLevels : IDatabaseMigrationRoutine
{
private readonly ILogger _logger;
@@ -26,15 +27,6 @@ namespace Jellyfin.Server.Migrations.Routines
_logger = loggerFactory.CreateLogger();
}
- ///
- public Guid Id => Guid.Parse("{98724538-EB11-40E3-931A-252C55BDDE7A}");
-
- ///
- public string Name => "MigrateRatingLevels";
-
- ///
- public bool PerformOnNewInstall => false;
-
///
public void Perform()
{
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
index 1b5fab7a89..7a23fcc98c 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
@@ -22,7 +22,10 @@ namespace Jellyfin.Server.Migrations.Routines
///
/// The migration routine for migrating the user database to EF Core.
///
+ [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;
}
- ///
- public Guid Id => Guid.Parse("5C4B82A2-F053-4009-BD05-B6FCAD82F14C");
-
- ///
- public string Name => "MigrateUserDatabase";
-
- ///
- public bool PerformOnNewInstall => false;
-
///
public void Perform()
{
diff --git a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs
index c5bbcd6f94..9031f2fdcf 100644
--- a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs
+++ b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs
@@ -24,7 +24,10 @@ namespace Jellyfin.Server.Migrations.Routines;
///
/// Migration to move extracted files to the new directories.
///
+[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 _logger;
@@ -58,15 +61,6 @@ public class MoveExtractedFiles : IMigrationRoutine
private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments");
- ///
- public Guid Id => new("9063b0Ef-CFF1-4EDC-9A13-74093681A89B");
-
- ///
- public string Name => "MoveExtractedFiles";
-
- ///
- public bool PerformOnNewInstall => false;
-
///
public void Perform()
{
diff --git a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
index a278138cee..6077080439 100644
--- a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
+++ b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
@@ -15,7 +15,10 @@ namespace Jellyfin.Server.Migrations.Routines;
///
/// Migration to move trickplay files to the new directory.
///
+[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;
}
- ///
- public Guid Id => new("9540D44A-D8DC-11EF-9CBB-B77274F77C52");
-
- ///
- public string Name => "MoveTrickplayFiles";
-
- ///
- public bool PerformOnNewInstall => true;
-
///
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);
diff --git a/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs
index 9cfaec46f8..1ef1dd45fe 100644
--- a/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs
+++ b/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs
@@ -7,7 +7,10 @@ namespace Jellyfin.Server.Migrations.Routines
///
/// Migration to initialize system configuration with the default plugin repository.
///
+ [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;
}
- ///
- public Guid Id => Guid.Parse("5F86E7F6-D966-4C77-849D-7A7B40B68C4E");
-
- ///
- public string Name => "ReaddDefaultPluginRepository";
-
- ///
- public bool PerformOnNewInstall => true;
-
///
public void Perform()
{
diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs b/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs
index 52fb93d594..477363e0de 100644
--- a/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs
+++ b/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs
@@ -8,7 +8,10 @@ namespace Jellyfin.Server.Migrations.Routines
///
/// Removes the old 'RemoveDownloadImagesInAdvance' from library options.
///
+ [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 _logger;
private readonly ILibraryManager _libraryManager;
@@ -19,15 +22,6 @@ namespace Jellyfin.Server.Migrations.Routines
_libraryManager = libraryManager;
}
- ///
- public Guid Id => Guid.Parse("{A81F75E0-8F43-416F-A5E8-516CCAB4D8CC}");
-
- ///
- public string Name => "RemoveDownloadImagesInAdvance";
-
- ///
- public bool PerformOnNewInstall => false;
-
///
public void Perform()
{
diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
index 7b0d9456dc..c80512deed 100644
--- a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
+++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
@@ -12,7 +12,10 @@ namespace Jellyfin.Server.Migrations.Routines
///
/// Remove duplicate entries which were caused by a bug where a file was considered to be an "Extra" to itself.
///
+ [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 _logger;
@@ -24,15 +27,6 @@ namespace Jellyfin.Server.Migrations.Routines
_paths = paths;
}
- ///
- public Guid Id => Guid.Parse("{ACBE17B7-8435-4A83-8B64-6FCF162CB9BD}");
-
- ///
- public string Name => "RemoveDuplicateExtras";
-
- ///
- public bool PerformOnNewInstall => false;
-
///
public void Perform()
{
diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs
index e183a1d63a..ce2be2755c 100644
--- a/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs
+++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs
@@ -11,7 +11,10 @@ namespace Jellyfin.Server.Migrations.Routines;
///
/// Remove duplicate playlist entries.
///
+[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;
}
- ///
- public Guid Id => Guid.Parse("{96C156A2-7A13-4B3B-A8B8-FB80C94D20C0}");
-
- ///
- public string Name => "RemoveDuplicatePlaylistChildren";
-
- ///
- public bool PerformOnNewInstall => false;
-
///
public void Perform()
{
diff --git a/Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs
index 7e8c8ac871..cf3f5433b4 100644
--- a/Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs
+++ b/Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs
@@ -6,7 +6,10 @@ namespace Jellyfin.Server.Migrations.Routines;
///
/// Migration to update the default Jellyfin plugin repository.
///
+[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;
}
- ///
- public Guid Id => new("852816E0-2712-49A9-9240-C6FC5FCAD1A8");
-
- ///
- public string Name => "UpdateDefaultPluginRepository10.9";
-
- ///
- public bool PerformOnNewInstall => true;
-
///
public void Perform()
{
diff --git a/Jellyfin.Server/Migrations/Stages/CodeMigration.cs b/Jellyfin.Server/Migrations/Stages/CodeMigration.cs
new file mode 100644
index 0000000000..1e4dfb237c
--- /dev/null
+++ b/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");
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Stages/JellyfinMigrationStageTypes.cs b/Jellyfin.Server/Migrations/Stages/JellyfinMigrationStageTypes.cs
new file mode 100644
index 0000000000..d90ad3d9be
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Stages/JellyfinMigrationStageTypes.cs
@@ -0,0 +1,26 @@
+namespace Jellyfin.Server.Migrations.Stages;
+
+///
+/// Defines the stages the supports.
+///
+#pragma warning disable CA1008 // Enums should have zero value
+public enum JellyfinMigrationStageTypes
+#pragma warning restore CA1008 // Enums should have zero value
+{
+ ///
+ /// Runs before services are initialised.
+ /// Reserved for migrations that are modifying the application server itself. Should be avoided if possible.
+ ///
+ PreInitialisation = 1,
+
+ ///
+ /// 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.
+ ///
+ CoreInitialisaition = 2,
+
+ ///
+ /// Runs after services has been registered and initialised. Last step before running the server.
+ ///
+ AppInitialisation = 3
+}
diff --git a/Jellyfin.Server/Migrations/Stages/MigrationStage.cs b/Jellyfin.Server/Migrations/Stages/MigrationStage.cs
new file mode 100644
index 0000000000..efcadbf006
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Stages/MigrationStage.cs
@@ -0,0 +1,16 @@
+using System.Collections.ObjectModel;
+
+namespace Jellyfin.Server.Migrations.Stages;
+
+///
+/// Defines a Stage that can be Invoked and Handled at different times from the code.
+///
+internal class MigrationStage : Collection
+{
+ public MigrationStage(JellyfinMigrationStageTypes stage)
+ {
+ Stage = stage;
+ }
+
+ public JellyfinMigrationStageTypes Stage { get; }
+}
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 511306755b..12903544d3 100644
--- a/Jellyfin.Server/Program.cs
+++ b/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());
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
}
}
+ ///
+ /// [Internal]Runs the startup Migrations.
+ ///
+ ///
+ /// Not intended to be used other then by jellyfin and its tests.
+ ///
+ /// Application Paths.
+ /// Startup Config.
+ /// A task.
+ 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(appPaths)
+ .AddSingleton(appPaths);
+ var startupService = migrationStartupServiceProvider.BuildServiceProvider();
+ var jellyfinMigrationService = ActivatorUtilities.CreateInstance(startupService);
+ await jellyfinMigrationService.CheckFirstTimeRunOrMigration(appPaths).ConfigureAwait(false);
+ await jellyfinMigrationService.MigrateStepAsync(Migrations.Stages.JellyfinMigrationStageTypes.PreInitialisation, startupService).ConfigureAwait(false);
+ }
+
+ ///
+ /// [Internal]Runs the Jellyfin migrator service with the Core stage.
+ ///
+ ///
+ /// Not intended to be used other then by jellyfin and its tests.
+ ///
+ /// The service provider.
+ /// The stage to run.
+ /// A task.
+ public static async Task ApplyCoreMigrationsAsync(IServiceProvider serviceProvider, Migrations.Stages.JellyfinMigrationStageTypes jellyfinMigrationStage)
+ {
+ var jellyfinMigrationService = ActivatorUtilities.CreateInstance(serviceProvider);
+ await jellyfinMigrationService.MigrateStepAsync(jellyfinMigrationStage, serviceProvider).ConfigureAwait(false);
+ }
+
///
/// Create the application configuration.
///
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs
index ef1bf1769d..156d9618e1 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs
@@ -76,6 +76,11 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider
///
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))
diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
index a7fec2960c..c09bce52da 100644
--- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
+++ b/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();
appHost.ServiceProvider = host.Services;
+ var applicationPaths = appHost.ServiceProvider.GetRequiredService();
+ Program.ApplyStartupMigrationAsync((ServerApplicationPaths)applicationPaths, appHost.ServiceProvider.GetRequiredService()).GetAwaiter().GetResult();
+ Program.ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.CoreInitialisaition).GetAwaiter().GetResult();
appHost.InitializeServices(Mock.Of()).GetAwaiter().GetResult();
+ Program.ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.AppInitialisation).GetAwaiter().GetResult();
host.Start();
appHost.RunStartupTasksAsync().GetAwaiter().GetResult();