Unified migration handling (#13950)

This commit is contained in:
JPVenson 2025-04-28 03:18:08 +03:00 committed by GitHub
parent 1c4b5199b8
commit e66c76fc34
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 555 additions and 528 deletions

View File

@ -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);

View File

@ -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();
}

View File

@ -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
{
}

View File

@ -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();
}
}

View File

@ -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; }
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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()
};
}
}
}

View File

@ -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;
}
}
}

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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)

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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);

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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");
}
}
}

View File

@ -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
}

View File

@ -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; }
}

View File

@ -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>

View File

@ -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))

View File

@ -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();