aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server/Migrations
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Server/Migrations')
-rw-r--r--Jellyfin.Server/Migrations/JellyfinMigrationAttribute.cs7
-rw-r--r--Jellyfin.Server/Migrations/JellyfinMigrationBackupAttribute.cs35
-rw-r--r--Jellyfin.Server/Migrations/JellyfinMigrationService.cs302
-rw-r--r--Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs2
-rw-r--r--Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs2
-rw-r--r--Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs2
-rw-r--r--Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/FixAudioData.cs27
-rw-r--r--Jellyfin.Server/Migrations/Routines/FixDates.cs168
-rw-r--r--Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs16
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs107
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateLibraryDbCompatibilityCheck.cs73
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs123
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs103
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs331
-rw-r--r--Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs56
-rw-r--r--Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs7
-rw-r--r--Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs59
-rw-r--r--Jellyfin.Server/Migrations/Routines/RefreshInternalDateModified.cs131
-rw-r--r--Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs61
-rw-r--r--Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs103
-rw-r--r--Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs74
-rw-r--r--Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs2
-rw-r--r--Jellyfin.Server/Migrations/Stages/CodeMigration.cs50
-rw-r--r--Jellyfin.Server/Migrations/Stages/JellyfinMigrationStageTypes.cs2
34 files changed, 1366 insertions, 497 deletions
diff --git a/Jellyfin.Server/Migrations/JellyfinMigrationAttribute.cs b/Jellyfin.Server/Migrations/JellyfinMigrationAttribute.cs
index f523bc76c..70e54125b 100644
--- a/Jellyfin.Server/Migrations/JellyfinMigrationAttribute.cs
+++ b/Jellyfin.Server/Migrations/JellyfinMigrationAttribute.cs
@@ -17,7 +17,9 @@ public sealed class JellyfinMigrationAttribute : Attribute
/// </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>
+#pragma warning disable CS0618 // Type or member is obsolete
public JellyfinMigrationAttribute(string order, string name) : this(order, name, null)
+#pragma warning restore CS0618 // Type or member is obsolete
{
}
@@ -27,6 +29,7 @@ public sealed class JellyfinMigrationAttribute : Attribute
/// <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>
+ [Obsolete("This Constructor should only be used for Legacy migrations. Use the (Order,Name) one for all new ones instead.")]
public JellyfinMigrationAttribute(string order, string name, string? key)
{
Order = DateTime.Parse(order, CultureInfo.InvariantCulture);
@@ -44,9 +47,9 @@ public sealed class JellyfinMigrationAttribute : Attribute
public bool RunMigrationOnSetup { get; set; }
/// <summary>
- /// Gets or Sets the stage the annoated migration should be executed at. Defaults to <see cref="JellyfinMigrationStageTypes.CoreInitialisaition"/>.
+ /// Gets or Sets the stage the annoated migration should be executed at. Defaults to <see cref="JellyfinMigrationStageTypes.CoreInitialisation"/>.
/// </summary>
- public JellyfinMigrationStageTypes Stage { get; set; } = JellyfinMigrationStageTypes.CoreInitialisaition;
+ public JellyfinMigrationStageTypes Stage { get; set; } = JellyfinMigrationStageTypes.CoreInitialisation;
/// <summary>
/// Gets the ordering of the migration.
diff --git a/Jellyfin.Server/Migrations/JellyfinMigrationBackupAttribute.cs b/Jellyfin.Server/Migrations/JellyfinMigrationBackupAttribute.cs
new file mode 100644
index 000000000..6c8da7e82
--- /dev/null
+++ b/Jellyfin.Server/Migrations/JellyfinMigrationBackupAttribute.cs
@@ -0,0 +1,35 @@
+using System;
+
+namespace Jellyfin.Server.Migrations;
+
+/// <summary>
+/// Marks an <see cref="JellyfinMigrationAttribute"/> migration and instructs the <see cref="JellyfinMigrationService"/> to perform a backup.
+/// </summary>
+[AttributeUsage(System.AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
+public sealed class JellyfinMigrationBackupAttribute : System.Attribute
+{
+ /// <summary>
+ /// Gets or Sets a value indicating whether a backup of the old library.db should be performed.
+ /// </summary>
+ public bool LegacyLibraryDb { get; set; }
+
+ /// <summary>
+ /// Gets or Sets a value indicating whether a backup of the Database should be performed.
+ /// </summary>
+ public bool JellyfinDb { get; set; }
+
+ /// <summary>
+ /// Gets or Sets a value indicating whether a backup of the metadata folder should be performed.
+ /// </summary>
+ public bool Metadata { get; set; }
+
+ /// <summary>
+ /// Gets or Sets a value indicating whether a backup of the Trickplay folder should be performed.
+ /// </summary>
+ public bool Trickplay { get; set; }
+
+ /// <summary>
+ /// Gets or Sets a value indicating whether a backup of the Subtitles folder should be performed.
+ /// </summary>
+ public bool Subtitles { get; set; }
+}
diff --git a/Jellyfin.Server/Migrations/JellyfinMigrationService.cs b/Jellyfin.Server/Migrations/JellyfinMigrationService.cs
index 46c22d16c..fe191916c 100644
--- a/Jellyfin.Server/Migrations/JellyfinMigrationService.cs
+++ b/Jellyfin.Server/Migrations/JellyfinMigrationService.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
@@ -7,13 +8,16 @@ using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Serialization;
using Jellyfin.Database.Implementations;
+using Jellyfin.Server.Implementations.SystemBackupService;
using Jellyfin.Server.Migrations.Stages;
+using Jellyfin.Server.ServerSetupApp;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.SystemBackupService;
using MediaBrowser.Model.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.Extensions.DependencyInjection;
+using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations;
@@ -23,21 +27,41 @@ namespace Jellyfin.Server.Migrations;
/// </summary>
internal class JellyfinMigrationService
{
+ private const string DbFilename = "library.db";
private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory;
private readonly ILoggerFactory _loggerFactory;
+ private readonly IStartupLogger _startupLogger;
+ private readonly IBackupService? _backupService;
+ private readonly IJellyfinDatabaseProvider? _jellyfinDatabaseProvider;
+ private readonly IApplicationPaths _applicationPaths;
+ private (string? LibraryDb, string? JellyfinDb, BackupManifestDto? FullBackup) _backupKey;
/// <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)
+ /// <param name="startupLogger">The startup logger for Startup UI intigration.</param>
+ /// <param name="applicationPaths">Application paths for library.db backup.</param>
+ /// <param name="backupService">The jellyfin backup service.</param>
+ /// <param name="jellyfinDatabaseProvider">The jellyfin database provider.</param>
+ public JellyfinMigrationService(
+ IDbContextFactory<JellyfinDbContext> dbContextFactory,
+ ILoggerFactory loggerFactory,
+ IStartupLogger<JellyfinMigrationService> startupLogger,
+ IApplicationPaths applicationPaths,
+ IBackupService? backupService = null,
+ IJellyfinDatabaseProvider? jellyfinDatabaseProvider = null)
{
_dbContextFactory = dbContextFactory;
_loggerFactory = loggerFactory;
+ _startupLogger = startupLogger;
+ _backupService = backupService;
+ _jellyfinDatabaseProvider = jellyfinDatabaseProvider;
+ _applicationPaths = applicationPaths;
#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>()))
+ .Select(e => (Type: e, Metadata: e.GetCustomAttribute<JellyfinMigrationAttribute>(), Backup: e.GetCustomAttributes<JellyfinMigrationBackupAttribute>()))
.Where(e => e.Metadata != null)
.GroupBy(e => e.Metadata!.Stage)
.Select(f =>
@@ -45,7 +69,13 @@ internal class JellyfinMigrationService
var stage = new MigrationStage(f.Key);
foreach (var item in f)
{
- stage.Add(new(item.Type, item.Metadata!));
+ JellyfinMigrationBackupAttribute? backupMetadata = null;
+ if (item.Backup?.Any() == true)
+ {
+ backupMetadata = item.Backup.Aggregate(MergeBackupAttributes);
+ }
+
+ stage.Add(new(item.Type, item.Metadata!, backupMetadata));
}
return stage;
@@ -55,14 +85,14 @@ internal class JellyfinMigrationService
private interface IInternalMigration
{
- Task PerformAsync(ILogger logger);
+ Task PerformAsync(IStartupLogger logger);
}
private HashSet<MigrationStage> Migrations { get; set; }
public async Task CheckFirstTimeRunOrMigration(IApplicationPaths appPaths)
{
- var logger = _loggerFactory.CreateLogger<JellyfinMigrationService>();
+ var logger = _startupLogger.With(_loggerFactory.CreateLogger<JellyfinMigrationService>()).BeginGroup($"Migration Startup");
logger.LogInformation("Initialise Migration service.");
var xmlSerializer = new MyXmlSerializer();
var serverConfig = File.Exists(appPaths.SystemConfigurationFilePath)
@@ -76,6 +106,13 @@ internal class JellyfinMigrationService
var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
+ var databaseCreator = dbContext.Database.GetService<IDatabaseCreator>() as IRelationalDatabaseCreator
+ ?? throw new InvalidOperationException("Jellyfin does only support relational databases.");
+ if (!await databaseCreator.ExistsAsync().ConfigureAwait(false))
+ {
+ await databaseCreator.CreateAsync().ConfigureAwait(false);
+ }
+
var historyRepository = dbContext.GetService<IHistoryRepository>();
await historyRepository.CreateIfNotExistsAsync().ConfigureAwait(false);
@@ -103,24 +140,44 @@ internal class JellyfinMigrationService
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))
+ try
{
- 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)
+ var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
{
- logger.LogInformation("Migrate migration {Key}-{Name}.", item.Migration.Key, item.Migration.Name);
- await dbContext.Database.ExecuteSqlRawAsync(item.Script).ConfigureAwait(false);
- }
+ var historyRepository = dbContext.GetService<IHistoryRepository>();
+ var appliedMigrations = await dbContext.Database.GetAppliedMigrationsAsync().ConfigureAwait(false);
+ var lastOldAppliedMigration = Migrations
+ .SelectMany(e => e.Where(e => e.Metadata.Key is not null)) // only consider migrations that have the key set as its the reference marker for legacy migrations.
+ .Where(e => migrationOptions.Applied.Any(f => f.Id.Equals(e.Metadata.Key!.Value)))
+ .Where(e => !appliedMigrations.Contains(e.BuildCodeMigrationId()))
+ .OrderBy(e => e.BuildCodeMigrationId())
+ .Last(); // this is the latest migration applied in the old migration.xml
+
+ IReadOnlyList<CodeMigration> oldMigrations = [
+ .. Migrations
+ .SelectMany(e => e)
+ .OrderBy(e => e.BuildCodeMigrationId())
+ .TakeWhile(e => e.BuildCodeMigrationId() != lastOldAppliedMigration.BuildCodeMigrationId()),
+ lastOldAppliedMigration
+ ];
+ // those are all migrations that had to run in the old migration system, even if not noted in the migration.xml file.
- logger.LogInformation("Rename old migration.xml to migration.xml.backup");
- File.Move(migrationConfigPath, Path.ChangeExtension(migrationConfigPath, ".xml.backup"), true);
+ 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);
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogCritical(ex, "Failed to apply migrations");
+ throw;
}
}
}
@@ -128,8 +185,7 @@ internal class JellyfinMigrationService
public async Task MigrateStepAsync(JellyfinMigrationStageTypes stage, IServiceProvider? serviceProvider)
{
- var logger = _loggerFactory.CreateLogger<JellyfinMigrationService>();
- logger.LogInformation("Migrate stage {Stage}.", stage);
+ var logger = _startupLogger.With(_loggerFactory.CreateLogger<JellyfinMigrationService>()).BeginGroup($"Migrate stage {stage}.");
ICollection<CodeMigration> migrationStage = (Migrations.FirstOrDefault(e => e.Stage == stage) as ICollection<CodeMigration>) ?? [];
var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
@@ -144,7 +200,7 @@ internal class JellyfinMigrationService
.ToArray();
(string Key, InternalDatabaseMigration Migration)[] pendingDatabaseMigrations = [];
- if (stage is JellyfinMigrationStageTypes.CoreInitialisaition)
+ if (stage is JellyfinMigrationStageTypes.CoreInitialisation)
{
pendingDatabaseMigrations = migrationsAssembly.Migrations.Where(f => appliedMigrations.All(e => e.MigrationId != f.Key))
.Select(e => (Key: e.Key, Migration: new InternalDatabaseMigration(e, dbContext)))
@@ -154,17 +210,64 @@ internal class JellyfinMigrationService
(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)
{
+ var migrationLogger = logger.With(_loggerFactory.CreateLogger(item.Migration.GetType().Name)).BeginGroup($"{item.Key}");
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);
+ migrationLogger.LogInformation("Perform migration {Name}", item.Key);
+ await item.Migration.PerformAsync(migrationLogger).ConfigureAwait(false);
+ migrationLogger.LogInformation("Migration {Name} was successfully applied", item.Key);
}
catch (Exception ex)
{
- logger.LogCritical(ex, "Migration {Name} failed", item.Key);
+ migrationLogger.LogCritical("Error: {Error}", ex.Message);
+ migrationLogger.LogError(ex, "Migration {Name} failed", item.Key);
+
+ if (_backupKey != default && _backupService is not null && _jellyfinDatabaseProvider is not null)
+ {
+ if (_backupKey.LibraryDb is not null)
+ {
+ migrationLogger.LogInformation("Attempt to rollback librarydb.");
+ try
+ {
+ var libraryDbPath = Path.Combine(_applicationPaths.DataPath, DbFilename);
+ File.Move(_backupKey.LibraryDb, libraryDbPath, true);
+ }
+ catch (Exception inner)
+ {
+ migrationLogger.LogCritical(inner, "Could not rollback {LibraryPath}. Manual intervention might be required to restore a operational state.", _backupKey.LibraryDb);
+ }
+ }
+
+ if (_backupKey.JellyfinDb is not null)
+ {
+ migrationLogger.LogInformation("Attempt to rollback JellyfinDb.");
+ try
+ {
+ await _jellyfinDatabaseProvider.RestoreBackupFast(_backupKey.JellyfinDb, CancellationToken.None).ConfigureAwait(false);
+ }
+ catch (Exception inner)
+ {
+ migrationLogger.LogCritical(inner, "Could not rollback {LibraryPath}. Manual intervention might be required to restore a operational state.", _backupKey.JellyfinDb);
+ }
+ }
+
+ if (_backupKey.FullBackup is not null)
+ {
+ migrationLogger.LogInformation("Attempt to rollback from backup.");
+ try
+ {
+ await _backupService.RestoreBackupAsync(_backupKey.FullBackup.Path).ConfigureAwait(false);
+ }
+ catch (Exception inner)
+ {
+ migrationLogger.LogCritical(inner, "Could not rollback from backup {Backup}. Manual intervention might be required to restore a operational state.", _backupKey.FullBackup.Path);
+ }
+ }
+ }
+
throw;
}
}
@@ -176,6 +279,143 @@ internal class JellyfinMigrationService
return Assembly.GetEntryAssembly()!.GetName().Version!.ToString();
}
+ public async Task CleanupSystemAfterMigration(ILogger logger)
+ {
+ if (_backupKey != default)
+ {
+ if (_backupKey.LibraryDb is not null)
+ {
+ logger.LogInformation("Attempt to cleanup librarydb backup.");
+ try
+ {
+ File.Delete(_backupKey.LibraryDb);
+ }
+ catch (Exception inner)
+ {
+ logger.LogCritical(inner, "Could not cleanup {LibraryPath}.", _backupKey.LibraryDb);
+ }
+ }
+
+ if (_backupKey.JellyfinDb is not null && _jellyfinDatabaseProvider is not null)
+ {
+ logger.LogInformation("Attempt to cleanup JellyfinDb backup.");
+ try
+ {
+ await _jellyfinDatabaseProvider.DeleteBackup(_backupKey.JellyfinDb).ConfigureAwait(false);
+ }
+ catch (Exception inner)
+ {
+ logger.LogCritical(inner, "Could not cleanup {LibraryPath}.", _backupKey.JellyfinDb);
+ }
+ }
+
+ if (_backupKey.FullBackup is not null)
+ {
+ logger.LogInformation("Attempt to cleanup from migration backup.");
+ try
+ {
+ File.Delete(_backupKey.FullBackup.Path);
+ }
+ catch (Exception inner)
+ {
+ logger.LogCritical(inner, "Could not cleanup backup {Backup}.", _backupKey.FullBackup.Path);
+ }
+ }
+ }
+ }
+
+ public async Task PrepareSystemForMigration(ILogger logger)
+ {
+ logger.LogInformation("Prepare system for possible migrations");
+ JellyfinMigrationBackupAttribute backupInstruction;
+ IReadOnlyList<HistoryRow> appliedMigrations;
+ var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ var historyRepository = dbContext.GetService<IHistoryRepository>();
+ var migrationsAssembly = dbContext.GetService<IMigrationsAssembly>();
+ appliedMigrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
+ backupInstruction = new JellyfinMigrationBackupAttribute()
+ {
+ JellyfinDb = migrationsAssembly.Migrations.Any(f => appliedMigrations.All(e => e.MigrationId != f.Key))
+ };
+ }
+
+ backupInstruction = Migrations.SelectMany(e => e)
+ .Where(e => appliedMigrations.All(f => f.MigrationId != e.BuildCodeMigrationId()))
+ .Select(e => e.BackupRequirements)
+ .Where(e => e is not null)
+ .Aggregate(backupInstruction, MergeBackupAttributes!);
+
+ if (backupInstruction.LegacyLibraryDb)
+ {
+ logger.LogInformation("A migration will attempt to modify the library.db, will attempt to backup the file now.");
+ // for legacy migrations that still operates on the library.db
+ var libraryDbPath = Path.Combine(_applicationPaths.DataPath, DbFilename);
+ if (File.Exists(libraryDbPath))
+ {
+ for (int i = 1; ; i++)
+ {
+ var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", libraryDbPath, i);
+ if (!File.Exists(bakPath))
+ {
+ try
+ {
+ logger.LogInformation("Backing up {Library} to {BackupPath}", DbFilename, bakPath);
+ File.Copy(libraryDbPath, bakPath);
+ _backupKey = (bakPath, _backupKey.JellyfinDb, _backupKey.FullBackup);
+ logger.LogInformation("{Library} backed up to {BackupPath}", DbFilename, bakPath);
+ break;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath);
+ throw;
+ }
+ }
+ }
+
+ logger.LogInformation("{Library} has been backed up as {BackupPath}", DbFilename, _backupKey.LibraryDb);
+ }
+ else
+ {
+ logger.LogError("Cannot make a backup of {Library} at path {BackupPath} because file could not be found at {LibraryPath}", DbFilename, libraryDbPath, _applicationPaths.DataPath);
+ }
+ }
+
+ if (backupInstruction.JellyfinDb && _jellyfinDatabaseProvider != null)
+ {
+ logger.LogInformation("A migration will attempt to modify the jellyfin.db, will attempt to backup the file now.");
+ _backupKey = (_backupKey.LibraryDb, await _jellyfinDatabaseProvider.MigrationBackupFast(CancellationToken.None).ConfigureAwait(false), _backupKey.FullBackup);
+ logger.LogInformation("Jellyfin database has been backed up as {BackupPath}", _backupKey.JellyfinDb);
+ }
+
+ if (_backupService is not null && (backupInstruction.Metadata || backupInstruction.Subtitles || backupInstruction.Trickplay))
+ {
+ logger.LogInformation("A migration will attempt to modify system resources. Will attempt to create backup now.");
+ _backupKey = (_backupKey.LibraryDb, _backupKey.JellyfinDb, await _backupService.CreateBackupAsync(new BackupOptionsDto()
+ {
+ Metadata = backupInstruction.Metadata,
+ Subtitles = backupInstruction.Subtitles,
+ Trickplay = backupInstruction.Trickplay,
+ Database = false // database backups are explicitly handled by the provider itself as the backup service requires parity with the current model
+ }).ConfigureAwait(false));
+ logger.LogInformation("Pre-Migration backup successfully created as {BackupKey}", _backupKey.FullBackup.Path);
+ }
+ }
+
+ private static JellyfinMigrationBackupAttribute MergeBackupAttributes(JellyfinMigrationBackupAttribute left, JellyfinMigrationBackupAttribute right)
+ {
+ return new JellyfinMigrationBackupAttribute()
+ {
+ JellyfinDb = left!.JellyfinDb || right!.JellyfinDb,
+ LegacyLibraryDb = left.LegacyLibraryDb || right!.LegacyLibraryDb,
+ Metadata = left.Metadata || right!.Metadata,
+ Subtitles = left.Subtitles || right!.Subtitles,
+ Trickplay = left.Trickplay || right!.Trickplay
+ };
+ }
+
private class InternalCodeMigration : IInternalMigration
{
private readonly CodeMigration _codeMigration;
@@ -189,9 +429,9 @@ internal class JellyfinMigrationService
_dbContext = dbContext;
}
- public async Task PerformAsync(ILogger logger)
+ public async Task PerformAsync(IStartupLogger logger)
{
- await _codeMigration.Perform(_serviceProvider, CancellationToken.None).ConfigureAwait(false);
+ await _codeMigration.Perform(_serviceProvider, logger, CancellationToken.None).ConfigureAwait(false);
var historyRepository = _dbContext.GetService<IHistoryRepository>();
var createScript = historyRepository.GetInsertScript(new HistoryRow(_codeMigration.BuildCodeMigrationId(), GetJellyfinVersion()));
@@ -210,7 +450,7 @@ internal class JellyfinMigrationService
_jellyfinDbContext = jellyfinDbContext;
}
- public async Task PerformAsync(ILogger logger)
+ public async Task PerformAsync(IStartupLogger logger)
{
var migrator = _jellyfinDbContext.GetService<IMigrator>();
await migrator.MigrateAsync(_databaseMigrationInfo.Key).ConfigureAwait(false);
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs
index a62523b88..fd472cff7 100644
--- a/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs
@@ -8,8 +8,8 @@ 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
+[JellyfinMigration("2025-04-20T00:00:00", nameof(CreateNetworkConfiguration), "9B354818-94D5-4B68-AC49-E35CB85F9D84", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)]
public class CreateNetworkConfiguration : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs
index 345569699..0141b43c9 100644
--- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs
@@ -10,8 +10,8 @@ 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
+[JellyfinMigration("2025-04-20T03:00:00", nameof(MigrateEncodingOptions), "A8E61960-7726-4450-8F3D-82C12DAABBCB", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)]
public class MigrateEncodingOptions : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs
index bdbf0c1ce..e8da9f515 100644
--- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs
@@ -9,8 +9,8 @@ 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
+[JellyfinMigration("2025-04-20T02:00:00", nameof(MigrateMusicBrainzTimeout), "A6DCACF4-C057-4Ef9-80D3-61CEF9DDB4F0", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)]
public class MigrateMusicBrainzTimeout : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs
index c0ca7896f..995b2bbf9 100644
--- a/Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs
@@ -9,8 +9,8 @@ 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
+[JellyfinMigration("2025-04-20T04:00:00", nameof(RenameEnableGroupingIntoCollections), "E73B777D-CD5C-4E71-957A-B86B3660B7CF", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)]
public class RenameEnableGroupingIntoCollections : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs b/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs
index 7e9243342..00d152b4b 100644
--- a/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs
+++ b/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs
@@ -7,8 +7,8 @@ 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
+[JellyfinMigration("2025-04-20T16:00:00", nameof(AddDefaultCastReceivers), "34A1A1C4-5572-418E-A2F8-32CDFE2668E8", RunMigrationOnSetup = true)]
public class AddDefaultCastReceivers : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs
index 603e01c18..8c8398a16 100644
--- a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs
+++ b/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs
@@ -7,8 +7,8 @@ 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
+ [JellyfinMigration("2025-04-20T09:00:00", nameof(AddDefaultPluginRepository), "EB58EBEE-9514-4B9B-8225-12E1A40020DF", RunMigrationOnSetup = true)]
public class AddDefaultPluginRepository : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
index 9d2a901cd..1326a6dc8 100644
--- a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
+++ b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
@@ -12,8 +12,8 @@ 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
+ [JellyfinMigration("2025-04-20T06:00:00", nameof(CreateUserLoggingConfigFile), "EF103419-8451-40D8-9F34-D1A8E93A1679")]
internal class CreateUserLoggingConfigFile : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
index ca9bf3264..acf2835fe 100644
--- a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
+++ b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
@@ -7,8 +7,8 @@ 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
+ [JellyfinMigration("2025-04-20T05:00:00", nameof(DisableTranscodingThrottling), "4124C2CD-E939-4FFB-9BE9-9B311C413638")]
internal class DisableTranscodingThrottling : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/Routines/FixAudioData.cs b/Jellyfin.Server/Migrations/Routines/FixAudioData.cs
index 6ebb5000e..05ded06ba 100644
--- a/Jellyfin.Server/Migrations/Routines/FixAudioData.cs
+++ b/Jellyfin.Server/Migrations/Routines/FixAudioData.cs
@@ -16,12 +16,12 @@ 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
+ [JellyfinMigration("2025-04-20T18:00:00", nameof(FixAudioData), "CF6FABC2-9FBE-4933-84A5-FFE52EF22A58")]
+ [JellyfinMigrationBackup(LegacyLibraryDb = true)]
internal class FixAudioData : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
- private const string DbFilename = "library.db";
private readonly ILogger<FixAudioData> _logger;
private readonly IServerApplicationPaths _applicationPaths;
private readonly IItemRepository _itemRepository;
@@ -39,29 +39,6 @@ namespace Jellyfin.Server.Migrations.Routines
/// <inheritdoc/>
public void Perform()
{
- var dbPath = Path.Combine(_applicationPaths.DataPath, DbFilename);
-
- // Back up the database before modifying any entries
- for (int i = 1; ; i++)
- {
- var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i);
- if (!File.Exists(bakPath))
- {
- try
- {
- _logger.LogInformation("Backing up {Library} to {BackupPath}", DbFilename, bakPath);
- File.Copy(dbPath, bakPath);
- _logger.LogInformation("{Library} backed up to {BackupPath}", DbFilename, bakPath);
- break;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath);
- throw;
- }
- }
- }
-
_logger.LogInformation("Backfilling audio lyrics data to database.");
var startIndex = 0;
var records = _itemRepository.GetCount(new InternalItemsQuery
diff --git a/Jellyfin.Server/Migrations/Routines/FixDates.cs b/Jellyfin.Server/Migrations/Routines/FixDates.cs
new file mode 100644
index 000000000..f112502b9
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/FixDates.cs
@@ -0,0 +1,168 @@
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Server.ServerSetupApp;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Migration to fix dates saved in the database to always be UTC.
+/// </summary>
+[JellyfinMigration("2025-06-20T18:00:00", nameof(FixDates))]
+public class FixDates : IAsyncMigrationRoutine
+{
+ private const int PageSize = 5000;
+
+ private readonly ILogger _logger;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FixDates"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="startupLogger">The startup logger for Startup UI integration.</param>
+ /// <param name="dbProvider">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
+ public FixDates(
+ ILogger<FixDates> logger,
+ IStartupLogger<FixDates> startupLogger,
+ IDbContextFactory<JellyfinDbContext> dbProvider)
+ {
+ _logger = startupLogger.With(logger);
+ _dbProvider = dbProvider;
+ }
+
+ /// <inheritdoc />
+ public async Task PerformAsync(CancellationToken cancellationToken)
+ {
+ if (!TimeZoneInfo.Local.Equals(TimeZoneInfo.Utc))
+ {
+ using var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ var sw = Stopwatch.StartNew();
+
+ await FixBaseItemsAsync(context, sw, cancellationToken).ConfigureAwait(false);
+ sw.Reset();
+ await FixChaptersAsync(context, sw, cancellationToken).ConfigureAwait(false);
+ sw.Reset();
+ await FixBaseItemImageInfos(context, sw, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ private async Task FixBaseItemsAsync(JellyfinDbContext context, Stopwatch sw, CancellationToken cancellationToken)
+ {
+ int itemCount = 0;
+
+ var baseQuery = context.BaseItems.OrderBy(e => e.Id);
+ var records = baseQuery.Count();
+ _logger.LogInformation("Fixing dates for {Count} BaseItems.", records);
+
+ sw.Start();
+ await foreach (var result in context.BaseItems.OrderBy(e => e.Id)
+ .WithPartitionProgress(
+ (partition) =>
+ _logger.LogInformation(
+ "Processing BaseItems batch {BatchNumber} ({ProcessedSoFar}/{TotalRecords}) - Time: {ElapsedTime}",
+ partition + 1,
+ Math.Min((partition + 1) * PageSize, records),
+ records,
+ sw.Elapsed))
+ .PartitionEagerAsync(PageSize, cancellationToken)
+ .WithCancellation(cancellationToken)
+ .ConfigureAwait(false))
+ {
+ result.DateCreated = ToUniversalTime(result.DateCreated);
+ result.DateLastMediaAdded = ToUniversalTime(result.DateLastMediaAdded);
+ result.DateLastRefreshed = ToUniversalTime(result.DateLastRefreshed);
+ result.DateLastSaved = ToUniversalTime(result.DateLastSaved);
+ result.DateModified = ToUniversalTime(result.DateModified);
+ itemCount++;
+ }
+
+ var saveCount = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
+ _logger.LogInformation("BaseItems: Processed {ItemCount} items, saved {SaveCount} changes in {ElapsedTime}", itemCount, saveCount, sw.Elapsed);
+ }
+
+ private async Task FixChaptersAsync(JellyfinDbContext context, Stopwatch sw, CancellationToken cancellationToken)
+ {
+ int itemCount = 0;
+
+ var baseQuery = context.Chapters;
+ var records = baseQuery.Count();
+ _logger.LogInformation("Fixing dates for {Count} Chapters.", records);
+
+ sw.Start();
+ await foreach (var result in context.Chapters.OrderBy(e => e.ItemId)
+ .WithPartitionProgress(
+ (partition) =>
+ _logger.LogInformation(
+ "Processing Chapter batch {BatchNumber} ({ProcessedSoFar}/{TotalRecords}) - Time: {ElapsedTime}",
+ partition + 1,
+ Math.Min((partition + 1) * PageSize, records),
+ records,
+ sw.Elapsed))
+ .PartitionEagerAsync(PageSize, cancellationToken)
+ .WithCancellation(cancellationToken)
+ .ConfigureAwait(false))
+ {
+ result.ImageDateModified = ToUniversalTime(result.ImageDateModified, true);
+ itemCount++;
+ }
+
+ var saveCount = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
+ _logger.LogInformation("Chapters: Processed {ItemCount} items, saved {SaveCount} changes in {ElapsedTime}", itemCount, saveCount, sw.Elapsed);
+ }
+
+ private async Task FixBaseItemImageInfos(JellyfinDbContext context, Stopwatch sw, CancellationToken cancellationToken)
+ {
+ int itemCount = 0;
+
+ var baseQuery = context.BaseItemImageInfos;
+ var records = baseQuery.Count();
+ _logger.LogInformation("Fixing dates for {Count} BaseItemImageInfos.", records);
+
+ sw.Start();
+ await foreach (var result in context.BaseItemImageInfos.OrderBy(e => e.Id)
+ .WithPartitionProgress(
+ (partition) =>
+ _logger.LogInformation(
+ "Processing BaseItemImageInfos batch {BatchNumber} ({ProcessedSoFar}/{TotalRecords}) - Time: {ElapsedTime}",
+ partition + 1,
+ Math.Min((partition + 1) * PageSize, records),
+ records,
+ sw.Elapsed))
+ .PartitionEagerAsync(PageSize, cancellationToken)
+ .WithCancellation(cancellationToken)
+ .ConfigureAwait(false))
+ {
+ result.DateModified = ToUniversalTime(result.DateModified);
+ itemCount++;
+ }
+
+ var saveCount = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
+ _logger.LogInformation("BaseItemImageInfos: Processed {ItemCount} items, saved {SaveCount} changes in {ElapsedTime}", itemCount, saveCount, sw.Elapsed);
+ }
+
+ private DateTime? ToUniversalTime(DateTime? dateTime, bool isUTC = false)
+ {
+ if (dateTime is null)
+ {
+ return null;
+ }
+
+ if (dateTime.Value.Year == 1 && dateTime.Value.Month == 1 && dateTime.Value.Day == 1)
+ {
+ return null;
+ }
+
+ if (dateTime.Value.Kind == DateTimeKind.Utc || isUTC)
+ {
+ return dateTime.Value;
+ }
+
+ return dateTime.Value.ToUniversalTime();
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
index f31c1afbd..56614ece3 100644
--- a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
+++ b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
@@ -13,8 +13,8 @@ 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
+[JellyfinMigration("2025-04-20T15:00:00", nameof(FixPlaylistOwner), "615DFA9E-2497-4DBB-A472-61938B752C5B")]
internal class FixPlaylistOwner : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
index 14089cac7..a954d307e 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
@@ -14,8 +14,8 @@ 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
+ [JellyfinMigration("2025-04-20T07:00:00", nameof(MigrateActivityLogDb), "3793eb59-bc8c-456c-8b9f-bd5a62a42978")]
public class MigrateActivityLogDb : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
index e4362f44d..c6699c21d 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
@@ -15,8 +15,8 @@ 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
+ [JellyfinMigration("2025-04-20T14:00:00", nameof(MigrateAuthenticationDb), "5BD72F41-E6F3-4F60-90AA-09869ABE0E22")]
public class MigrateAuthenticationDb : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index 49ed01d6b..0d9952ce9 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -20,8 +20,8 @@ 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
+ [JellyfinMigration("2025-04-20T12:00:00", nameof(MigrateDisplayPreferencesDb), "06387815-C3CC-421F-A888-FB5F9992BEA8")]
public class MigrateDisplayPreferencesDb : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs
index c5bc70278..c199ee4d6 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs
@@ -9,6 +9,7 @@ using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions.Json;
+using Jellyfin.Server.ServerSetupApp;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using Microsoft.EntityFrameworkCore;
@@ -19,10 +20,10 @@ 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")]
+[JellyfinMigration("2025-04-21T00:00:00", nameof(MigrateKeyframeData))]
public class MigrateKeyframeData : IDatabaseMigrationRoutine
{
- private readonly ILogger<MigrateKeyframeData> _logger;
+ private readonly IStartupLogger _logger;
private readonly IApplicationPaths _appPaths;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
@@ -30,15 +31,15 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine
/// <summary>
/// Initializes a new instance of the <see cref="MigrateKeyframeData"/> class.
/// </summary>
- /// <param name="logger">The logger.</param>
+ /// <param name="startupLogger">The startup logger for Startup UI intigration.</param>
/// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
/// <param name="dbProvider">The EFCore db factory.</param>
public MigrateKeyframeData(
- ILogger<MigrateKeyframeData> logger,
+ IStartupLogger<MigrateKeyframeData> startupLogger,
IApplicationPaths appPaths,
IDbContextFactory<JellyfinDbContext> dbProvider)
{
- _logger = logger;
+ _logger = startupLogger;
_appPaths = appPaths;
_dbProvider = dbProvider;
}
@@ -73,6 +74,11 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine
}
offset += Limit;
+ if (offset > records)
+ {
+ offset = records;
+ }
+
_logger.LogInformation("Checked: {Count} - Imported: {Items} - Time: {Time}", offset, itemCount, sw.Elapsed);
} while (offset < records);
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
index 8374508e6..e04a2737a 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
@@ -9,26 +9,17 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
-using System.Threading;
using Emby.Server.Implementations.Data;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using Jellyfin.Server.Implementations.Item;
+using Jellyfin.Server.ServerSetupApp;
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;
@@ -38,12 +29,13 @@ 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")]
+[JellyfinMigration("2025-04-20T20:00:00", nameof(MigrateLibraryDb))]
+[JellyfinMigrationBackup(JellyfinDb = true, LegacyLibraryDb = true)]
internal class MigrateLibraryDb : IDatabaseMigrationRoutine
{
private const string DbFilename = "library.db";
- private readonly ILogger<MigrateLibraryDb> _logger;
+ private readonly IStartupLogger _logger;
private readonly IServerApplicationPaths _paths;
private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider;
private readonly IDbContextFactory<JellyfinDbContext> _provider;
@@ -51,19 +43,17 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
/// <summary>
/// Initializes a new instance of the <see cref="MigrateLibraryDb"/> class.
/// </summary>
- /// <param name="logger">The logger.</param>
+ /// <param name="startupLogger">The startup logger for Startup UI intigration.</param>
/// <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,
+ IStartupLogger<MigrateLibraryDb> startupLogger,
IDbContextFactory<JellyfinDbContext> provider,
IServerApplicationPaths paths,
- IJellyfinDatabaseProvider jellyfinDatabaseProvider,
- IServiceProvider serviceProvider)
+ IJellyfinDatabaseProvider jellyfinDatabaseProvider)
{
- _logger = logger;
+ _logger = startupLogger;
_provider = provider;
_paths = paths;
_jellyfinDatabaseProvider = jellyfinDatabaseProvider;
@@ -100,11 +90,14 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
operation.JellyfinDbContext.AncestorIds.ExecuteDelete();
}
+ // notify the other migration to just silently abort because the fix has been applied here already.
+ ReseedFolderFlag.RerunGuardFlag = true;
+
var legacyBaseItemWithUserKeys = new Dictionary<string, BaseItemEntity>();
connection.Open();
var baseItemIds = new HashSet<Guid>();
- using (var operation = GetPreparedDbContext("moving TypedBaseItem"))
+ using (var operation = GetPreparedDbContext("Moving TypedBaseItem"))
{
const string typedBaseItemsQuery =
"""
@@ -115,7 +108,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId,
DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, SeasonName, SeasonId, SeriesId,
PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate,
- ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName, CleanName, UnratedType FROM TypedBaseItems
+ ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName, CleanName, UnratedType, IsFolder FROM TypedBaseItems
""";
using (new TrackedMigrationStep("Loading TypedBaseItems", _logger))
{
@@ -131,13 +124,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
}
}
- using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.BaseItems.Local.Count} BaseItem entries", _logger))
+ using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.BaseItems.Local.Count} BaseItem entries", _logger))
{
operation.JellyfinDbContext.SaveChanges();
}
}
- using (var operation = GetPreparedDbContext("moving ItemValues"))
+ using (var operation = GetPreparedDbContext("Moving ItemValues"))
{
// do not migrate inherited types as they are now properly mapped in search and lookup.
const string itemValueQuery =
@@ -148,7 +141,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
// EFCores local lookup sucks. We cannot use context.ItemValues.Local here because its just super slow.
var localItems = new Dictionary<(int Type, string Value), (Database.Implementations.Entities.ItemValue ItemValue, List<Guid> ItemIds)>();
- using (new TrackedMigrationStep("loading ItemValues", _logger))
+ using (new TrackedMigrationStep("Loading ItemValues", _logger))
{
foreach (SqliteDataReader dto in connection.Query(itemValueQuery))
{
@@ -176,13 +169,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
}
}
- using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.ItemValues.Local.Count} ItemValues entries", _logger))
+ using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.ItemValues.Local.Count} ItemValues entries", _logger))
{
operation.JellyfinDbContext.SaveChanges();
}
}
- using (var operation = GetPreparedDbContext("moving UserData"))
+ using (var operation = GetPreparedDbContext("Moving UserData"))
{
var queryResult = connection.Query(
"""
@@ -191,14 +184,14 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.UserDataKey = UserDatas.key)
""");
- using (new TrackedMigrationStep("loading UserData", _logger))
+ using (new TrackedMigrationStep("Loading UserData", _logger))
{
- var users = operation.JellyfinDbContext.Users.AsNoTracking().ToImmutableArray();
+ var users = operation.JellyfinDbContext.Users.AsNoTracking().ToArray();
var userIdBlacklist = new HashSet<int>();
foreach (var entity in queryResult)
{
- var userData = GetUserData(users, entity, userIdBlacklist);
+ var userData = GetUserData(users, entity, userIdBlacklist, _logger);
if (userData is null)
{
var userDataId = entity.GetString(0);
@@ -222,19 +215,17 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
userData.ItemId = refItem.Id;
operation.JellyfinDbContext.UserData.Add(userData);
}
-
- users.Clear();
}
legacyBaseItemWithUserKeys.Clear();
- using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.UserData.Local.Count} UserData entries", _logger))
+ using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.UserData.Local.Count} UserData entries", _logger))
{
operation.JellyfinDbContext.SaveChanges();
}
}
- using (var operation = GetPreparedDbContext("moving MediaStreamInfos"))
+ using (var operation = GetPreparedDbContext("Moving MediaStreamInfos"))
{
const string mediaStreamQuery =
"""
@@ -247,7 +238,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = MediaStreams.ItemId)
""";
- using (new TrackedMigrationStep("loading MediaStreamInfos", _logger))
+ using (new TrackedMigrationStep("Loading MediaStreamInfos", _logger))
{
foreach (SqliteDataReader dto in connection.Query(mediaStreamQuery))
{
@@ -255,13 +246,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
}
}
- using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.MediaStreamInfos.Local.Count} MediaStreamInfos entries", _logger))
+ using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.MediaStreamInfos.Local.Count} MediaStreamInfos entries", _logger))
{
operation.JellyfinDbContext.SaveChanges();
}
}
- using (var operation = GetPreparedDbContext("moving AttachmentStreamInfos"))
+ using (var operation = GetPreparedDbContext("Moving AttachmentStreamInfos"))
{
const string mediaAttachmentQuery =
"""
@@ -270,7 +261,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = mediaattachments.ItemId)
""";
- using (new TrackedMigrationStep("loading AttachmentStreamInfos", _logger))
+ using (new TrackedMigrationStep("Loading AttachmentStreamInfos", _logger))
{
foreach (SqliteDataReader dto in connection.Query(mediaAttachmentQuery))
{
@@ -278,13 +269,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
}
}
- using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.AttachmentStreamInfos.Local.Count} AttachmentStreamInfos entries", _logger))
+ using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.AttachmentStreamInfos.Local.Count} AttachmentStreamInfos entries", _logger))
{
operation.JellyfinDbContext.SaveChanges();
}
}
- using (var operation = GetPreparedDbContext("moving People"))
+ using (var operation = GetPreparedDbContext("Moving People"))
{
const string personsQuery =
"""
@@ -294,14 +285,14 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
var peopleCache = new Dictionary<string, (People Person, List<PeopleBaseItemMap> Items)>();
- using (new TrackedMigrationStep("loading People", _logger))
+ using (new TrackedMigrationStep("Loading People", _logger))
{
foreach (SqliteDataReader reader in connection.Query(personsQuery))
{
var itemId = reader.GetGuid(0);
if (!baseItemIds.Contains(itemId))
{
- _logger.LogError("Dont save person {0} because its not in use by any BaseItem", reader.GetString(1));
+ _logger.LogError("Not saving person {0} because it's not in use by any BaseItem", reader.GetString(1));
continue;
}
@@ -340,13 +331,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
peopleCache.Clear();
}
- using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.Peoples.Local.Count} People entries and {operation.JellyfinDbContext.PeopleBaseItemMap.Local.Count} maps", _logger))
+ using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.Peoples.Local.Count} People entries and {operation.JellyfinDbContext.PeopleBaseItemMap.Local.Count} maps", _logger))
{
operation.JellyfinDbContext.SaveChanges();
}
}
- using (var operation = GetPreparedDbContext("moving Chapters"))
+ using (var operation = GetPreparedDbContext("Moving Chapters"))
{
const string chapterQuery =
"""
@@ -354,7 +345,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = Chapters2.ItemId)
""";
- using (new TrackedMigrationStep("loading Chapters", _logger))
+ using (new TrackedMigrationStep("Loading Chapters", _logger))
{
foreach (SqliteDataReader dto in connection.Query(chapterQuery))
{
@@ -363,13 +354,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
}
}
- using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.Chapters.Local.Count} Chapters entries", _logger))
+ using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.Chapters.Local.Count} Chapters entries", _logger))
{
operation.JellyfinDbContext.SaveChanges();
}
}
- using (var operation = GetPreparedDbContext("moving AncestorIds"))
+ using (var operation = GetPreparedDbContext("Moving AncestorIds"))
{
const string ancestorIdsQuery =
"""
@@ -380,7 +371,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.AncestorId)
""";
- using (new TrackedMigrationStep("loading AncestorIds", _logger))
+ using (new TrackedMigrationStep("Loading AncestorIds", _logger))
{
foreach (SqliteDataReader dto in connection.Query(ancestorIdsQuery))
{
@@ -389,7 +380,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
}
}
- using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.AncestorIds.Local.Count} AncestorId entries", _logger))
+ using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.AncestorIds.Local.Count} AncestorId entries", _logger))
{
operation.JellyfinDbContext.SaveChanges();
}
@@ -414,19 +405,20 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
return new DatabaseMigrationStep(dbContext, operationName, _logger);
}
- private UserData? GetUserData(ImmutableArray<User> users, SqliteDataReader dto, HashSet<int> userIdBlacklist)
+ internal static UserData? GetUserData(User[] users, SqliteDataReader dto, HashSet<int> userIdBlacklist, ILogger logger)
{
var internalUserId = dto.GetInt32(1);
- var user = users.FirstOrDefault(e => e.InternalId == internalUserId);
+ if (userIdBlacklist.Contains(internalUserId))
+ {
+ return null;
+ }
+ var user = users.FirstOrDefault(e => e.InternalId == internalUserId);
if (user is null)
{
- if (userIdBlacklist.Contains(internalUserId))
- {
- return null;
- }
+ userIdBlacklist.Add(internalUserId);
- _logger.LogError("Tried to find user with index '{Idx}' but there are only '{MaxIdx}' users.", internalUserId, users.Length);
+ logger.LogError("Tried to find user with index '{Idx}' but there are only '{MaxIdx}' users.", internalUserId, users.Length);
return null;
}
@@ -1178,7 +1170,12 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
entity.UnratedType = unratedType;
}
- var baseItem = BaseItemRepository.DeserialiseBaseItem(entity, _logger, null, false);
+ if (reader.TryGetBoolean(index++, out var isFolder))
+ {
+ entity.IsFolder = isFolder;
+ }
+
+ var baseItem = BaseItemRepository.DeserializeBaseItem(entity, _logger, null, false);
var dataKeys = baseItem.GetUserDataKeys();
userDataKeys.AddRange(dataKeys);
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDbCompatibilityCheck.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDbCompatibilityCheck.cs
new file mode 100644
index 000000000..d4cc9bbee
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDbCompatibilityCheck.cs
@@ -0,0 +1,73 @@
+#pragma warning disable RS0030 // Do not use banned APIs
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Server.ServerSetupApp;
+using MediaBrowser.Controller;
+using Microsoft.Data.Sqlite;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// The migration routine for checking if the current instance of Jellyfin is compatiable to be upgraded.
+/// </summary>
+[JellyfinMigration("2025-04-20T19:30:00", nameof(MigrateLibraryDbCompatibilityCheck))]
+public class MigrateLibraryDbCompatibilityCheck : IAsyncMigrationRoutine
+{
+ private const string DbFilename = "library.db";
+ private readonly IStartupLogger _logger;
+ private readonly IServerApplicationPaths _paths;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MigrateLibraryDbCompatibilityCheck"/> class.
+ /// </summary>
+ /// <param name="startupLogger">The startup logger.</param>
+ /// <param name="paths">The Path service.</param>
+ public MigrateLibraryDbCompatibilityCheck(IStartupLogger<MigrateLibraryDbCompatibilityCheck> startupLogger, IServerApplicationPaths paths)
+ {
+ _logger = startupLogger;
+ _paths = paths;
+ }
+
+ /// <inheritdoc/>
+ public async Task PerformAsync(CancellationToken cancellationToken)
+ {
+ 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");
+ await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
+ CheckMigratableVersion(connection);
+ await connection.CloseAsync().ConfigureAwait(false);
+ }
+
+ private static void CheckMigratableVersion(SqliteConnection connection)
+ {
+ CheckColumnExistance(connection, "TypedBaseItems", "lufs");
+ CheckColumnExistance(connection, "TypedBaseItems", "normalizationgain");
+ CheckColumnExistance(connection, "mediastreams", "dvversionmajor");
+
+ static void CheckColumnExistance(SqliteConnection connection, string table, string column)
+ {
+ using (var cmd = connection.CreateCommand())
+ {
+#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities
+ cmd.CommandText = $"Select COUNT(1) FROM pragma_table_xinfo('{table}') WHERE lower(name) = '{column}';";
+#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities
+ var result = cmd.ExecuteScalar()!;
+ if (!result.Equals(1L))
+ {
+ throw new InvalidOperationException("Your database does not meet the required standard. Only upgrades from server version 10.9.11 or above are supported. Please upgrade first to server version 10.10.7 before attempting to upgrade afterwards to 10.11");
+ }
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs
new file mode 100644
index 000000000..8a0a1741f
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs
@@ -0,0 +1,123 @@
+#pragma warning disable RS0030 // Do not use banned APIs
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Server.Implementations.Data;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Server.Implementations.Item;
+using Jellyfin.Server.ServerSetupApp;
+using MediaBrowser.Controller;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+[JellyfinMigration("2025-06-18T01:00:00", nameof(MigrateLibraryUserData))]
+[JellyfinMigrationBackup(JellyfinDb = true)]
+internal class MigrateLibraryUserData : IAsyncMigrationRoutine
+{
+ private const string DbFilename = "library.db.old";
+
+ private readonly IStartupLogger _logger;
+ private readonly IServerApplicationPaths _paths;
+ private readonly IDbContextFactory<JellyfinDbContext> _provider;
+
+ public MigrateLibraryUserData(
+ IStartupLogger<MigrateLibraryDb> startupLogger,
+ IDbContextFactory<JellyfinDbContext> provider,
+ IServerApplicationPaths paths)
+ {
+ _logger = startupLogger;
+ _provider = provider;
+ _paths = paths;
+ }
+
+ public async Task PerformAsync(CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Migrating the userdata from library.db.old may take a while, do not stop Jellyfin.");
+
+ var dataPath = _paths.DataPath;
+ var libraryDbPath = Path.Combine(dataPath, DbFilename);
+ if (!File.Exists(libraryDbPath))
+ {
+ _logger.LogError("Cannot migrate userdata from {LibraryDb} as it does not exist. This migration expects the MigrateLibraryDb to run first.", libraryDbPath);
+ return;
+ }
+
+ var dbContext = await _provider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ if (!await dbContext.BaseItems.AnyAsync(e => e.Id == BaseItemRepository.PlaceholderId, cancellationToken).ConfigureAwait(false))
+ {
+ // the placeholder baseitem has been deleted by the librarydb migration so we need to readd it.
+ await dbContext.BaseItems.AddAsync(
+ new Database.Implementations.Entities.BaseItemEntity()
+ {
+ Id = BaseItemRepository.PlaceholderId,
+ Type = "PLACEHOLDER",
+ Name = "This is a placeholder item for UserData that has been detacted from its original item"
+ },
+ cancellationToken)
+ .ConfigureAwait(false);
+ await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ var users = dbContext.Users.AsNoTracking().ToArray();
+ var userIdBlacklist = new HashSet<int>();
+ using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly");
+ var retentionDate = DateTime.UtcNow;
+
+ var queryResult = connection.Query(
+"""
+ SELECT key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex FROM UserDatas
+
+ WHERE NOT EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.UserDataKey = UserDatas.key)
+""");
+
+ var importedUserData = new Dictionary<Guid, List<UserData>>();
+ foreach (var entity in queryResult)
+ {
+ var userData = MigrateLibraryDb.GetUserData(users, entity, userIdBlacklist, _logger);
+ if (userData is null)
+ {
+ var userDataId = entity.GetString(0);
+ var internalUserId = entity.GetInt32(1);
+
+ if (!userIdBlacklist.Contains(internalUserId))
+ {
+ _logger.LogError("Was not able to migrate user data with key {0} because its id {InternalId} does not match any existing user.", userDataId, internalUserId);
+ userIdBlacklist.Add(internalUserId);
+ }
+
+ continue;
+ }
+
+ var ogId = userData.ItemId;
+ userData.ItemId = BaseItemRepository.PlaceholderId;
+ userData.RetentionDate = retentionDate;
+ if (!importedUserData.TryGetValue(ogId, out var importUserData))
+ {
+ importUserData = [];
+ importedUserData[ogId] = importUserData;
+ }
+
+ importUserData.Add(userData);
+ }
+
+ foreach (var item in importedUserData)
+ {
+ await dbContext.UserData.Where(e => e.ItemId == item.Key).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
+ dbContext.UserData.AddRange(item.Value.DistinctBy(e => e.CustomDataKey)); // old userdata can have fucked up duplicates
+ }
+
+ _logger.LogInformation("Try saving {NewSaved} UserData entries.", dbContext.UserData.Local.Count);
+ await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
index 96276e9b1..2a6db01cf 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
@@ -1,66 +1,69 @@
using System;
using System.Linq;
using Jellyfin.Database.Implementations;
+using Jellyfin.Server.ServerSetupApp;
using MediaBrowser.Model.Globalization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Server.Migrations.Routines
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Migrate rating levels.
+/// </summary>
+#pragma warning disable CS0618 // Type or member is obsolete
+[JellyfinMigration("2025-04-20T22:00:00", nameof(MigrateRatingLevels))]
+[JellyfinMigrationBackup(JellyfinDb = true)]
+#pragma warning restore CS0618 // Type or member is obsolete
+internal class MigrateRatingLevels : IDatabaseMigrationRoutine
{
- /// <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;
- private readonly IDbContextFactory<JellyfinDbContext> _provider;
- private readonly ILocalizationManager _localizationManager;
+ private readonly IStartupLogger _logger;
+ private readonly IDbContextFactory<JellyfinDbContext> _provider;
+ private readonly ILocalizationManager _localizationManager;
- public MigrateRatingLevels(
- IDbContextFactory<JellyfinDbContext> provider,
- ILoggerFactory loggerFactory,
- ILocalizationManager localizationManager)
- {
- _provider = provider;
- _localizationManager = localizationManager;
- _logger = loggerFactory.CreateLogger<MigrateRatingLevels>();
- }
+ public MigrateRatingLevels(
+ IDbContextFactory<JellyfinDbContext> provider,
+ IStartupLogger<MigrateRatingLevels> logger,
+ ILocalizationManager localizationManager)
+ {
+ _provider = provider;
+ _localizationManager = localizationManager;
+ _logger = logger;
+ }
- /// <inheritdoc/>
- public void Perform()
+ /// <inheritdoc/>
+ public void Perform()
+ {
+ _logger.LogInformation("Recalculating parental rating levels based on rating string.");
+ using var context = _provider.CreateDbContext();
+ using var transaction = context.Database.BeginTransaction();
+ var ratings = context.BaseItems.AsNoTracking().Select(e => e.OfficialRating).Distinct();
+ foreach (var rating in ratings)
{
- _logger.LogInformation("Recalculating parental rating levels based on rating string.");
- using var context = _provider.CreateDbContext();
- using var transaction = context.Database.BeginTransaction();
- var ratings = context.BaseItems.AsNoTracking().Select(e => e.OfficialRating).Distinct();
- foreach (var rating in ratings)
+ if (string.IsNullOrEmpty(rating))
{
- if (string.IsNullOrEmpty(rating))
- {
- int? value = null;
- context.BaseItems
- .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty)
- .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingValue, value));
- context.BaseItems
- .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty)
- .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingSubValue, value));
- }
- else
- {
- var ratingValue = _localizationManager.GetRatingScore(rating);
- var score = ratingValue?.Score;
- var subScore = ratingValue?.SubScore;
- context.BaseItems
- .Where(e => e.OfficialRating == rating)
- .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingValue, score));
- context.BaseItems
- .Where(e => e.OfficialRating == rating)
- .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingSubValue, subScore));
- }
+ int? value = null;
+ context.BaseItems
+ .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty)
+ .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingValue, value));
+ context.BaseItems
+ .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty)
+ .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingSubValue, value));
+ }
+ else
+ {
+ var ratingValue = _localizationManager.GetRatingScore(rating);
+ var score = ratingValue?.Score;
+ var subScore = ratingValue?.SubScore;
+ context.BaseItems
+ .Where(e => e.OfficialRating == rating)
+ .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingValue, score));
+ context.BaseItems
+ .Where(e => e.OfficialRating == rating)
+ .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingSubValue, subScore));
}
-
- transaction.Commit();
}
+
+ transaction.Commit();
}
}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
index 7a23fcc98..e5584fb94 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
@@ -17,201 +17,200 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using JsonSerializer = System.Text.Json.JsonSerializer;
-namespace Jellyfin.Server.Migrations.Routines
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// The migration routine for migrating the user database to EF Core.
+/// </summary>
+#pragma warning disable CS0618 // Type or member is obsolete
+[JellyfinMigration("2025-04-20T10:00:00", nameof(MigrateUserDb), "5C4B82A2-F053-4009-BD05-B6FCAD82F14C")]
+public class MigrateUserDb : IMigrationRoutine
+#pragma warning restore CS0618 // Type or member is obsolete
{
+ private const string DbFilename = "users.db";
+
+ private readonly ILogger<MigrateUserDb> _logger;
+ private readonly IServerApplicationPaths _paths;
+ private readonly IDbContextFactory<JellyfinDbContext> _provider;
+ private readonly IXmlSerializer _xmlSerializer;
+
/// <summary>
- /// The migration routine for migrating the user database to EF Core.
+ /// Initializes a new instance of the <see cref="MigrateUserDb"/> class.
/// </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
+ /// <param name="logger">The logger.</param>
+ /// <param name="paths">The server application paths.</param>
+ /// <param name="provider">The database provider.</param>
+ /// <param name="xmlSerializer">The xml serializer.</param>
+ public MigrateUserDb(
+ ILogger<MigrateUserDb> logger,
+ IServerApplicationPaths paths,
+ IDbContextFactory<JellyfinDbContext> provider,
+ IXmlSerializer xmlSerializer)
{
- private const string DbFilename = "users.db";
-
- private readonly ILogger<MigrateUserDb> _logger;
- private readonly IServerApplicationPaths _paths;
- private readonly IDbContextFactory<JellyfinDbContext> _provider;
- private readonly IXmlSerializer _xmlSerializer;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="MigrateUserDb"/> class.
- /// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="paths">The server application paths.</param>
- /// <param name="provider">The database provider.</param>
- /// <param name="xmlSerializer">The xml serializer.</param>
- public MigrateUserDb(
- ILogger<MigrateUserDb> logger,
- IServerApplicationPaths paths,
- IDbContextFactory<JellyfinDbContext> provider,
- IXmlSerializer xmlSerializer)
- {
- _logger = logger;
- _paths = paths;
- _provider = provider;
- _xmlSerializer = xmlSerializer;
- }
+ _logger = logger;
+ _paths = paths;
+ _provider = provider;
+ _xmlSerializer = xmlSerializer;
+ }
- /// <inheritdoc/>
- public void Perform()
+ /// <inheritdoc/>
+ public void Perform()
+ {
+ var dataPath = _paths.DataPath;
+ _logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin.");
+
+ using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
{
- var dataPath = _paths.DataPath;
- _logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin.");
+ connection.Open();
+ using var dbContext = _provider.CreateDbContext();
- using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
- {
- connection.Open();
- using var dbContext = _provider.CreateDbContext();
+ var queryResult = connection.Query("SELECT * FROM LocalUsersv2");
- var queryResult = connection.Query("SELECT * FROM LocalUsersv2");
+ dbContext.RemoveRange(dbContext.Users);
+ dbContext.SaveChanges();
- dbContext.RemoveRange(dbContext.Users);
- dbContext.SaveChanges();
+ foreach (var entry in queryResult)
+ {
+ UserMockup? mockup = JsonSerializer.Deserialize<UserMockup>(entry.GetStream(2), JsonDefaults.Options);
+ if (mockup is null)
+ {
+ continue;
+ }
- foreach (var entry in queryResult)
+ var userDataDir = Path.Combine(_paths.UserConfigurationDirectoryPath, mockup.Name);
+
+ var configPath = Path.Combine(userDataDir, "config.xml");
+ var config = File.Exists(configPath)
+ ? (UserConfiguration?)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), configPath) ?? new UserConfiguration()
+ : new UserConfiguration();
+
+ var policyPath = Path.Combine(userDataDir, "policy.xml");
+ var policy = File.Exists(policyPath)
+ ? (UserPolicy?)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), policyPath) ?? new UserPolicy()
+ : new UserPolicy();
+ policy.AuthenticationProviderId = policy.AuthenticationProviderId?.Replace(
+ "Emby.Server.Implementations.Library",
+ "Jellyfin.Server.Implementations.Users",
+ StringComparison.Ordinal)
+ ?? typeof(DefaultAuthenticationProvider).FullName;
+
+ policy.PasswordResetProviderId = typeof(DefaultPasswordResetProvider).FullName;
+ int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
{
- UserMockup? mockup = JsonSerializer.Deserialize<UserMockup>(entry.GetStream(2), JsonDefaults.Options);
- if (mockup is null)
- {
- continue;
- }
-
- var userDataDir = Path.Combine(_paths.UserConfigurationDirectoryPath, mockup.Name);
-
- var configPath = Path.Combine(userDataDir, "config.xml");
- var config = File.Exists(configPath)
- ? (UserConfiguration?)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), configPath) ?? new UserConfiguration()
- : new UserConfiguration();
-
- var policyPath = Path.Combine(userDataDir, "policy.xml");
- var policy = File.Exists(policyPath)
- ? (UserPolicy?)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), policyPath) ?? new UserPolicy()
- : new UserPolicy();
- policy.AuthenticationProviderId = policy.AuthenticationProviderId?.Replace(
- "Emby.Server.Implementations.Library",
- "Jellyfin.Server.Implementations.Users",
- StringComparison.Ordinal)
- ?? typeof(DefaultAuthenticationProvider).FullName;
-
- policy.PasswordResetProviderId = typeof(DefaultPasswordResetProvider).FullName;
- int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
- {
- -1 => null,
- 0 => 3,
- _ => policy.LoginAttemptsBeforeLockout
- };
+ -1 => null,
+ 0 => 3,
+ _ => policy.LoginAttemptsBeforeLockout
+ };
- var user = new User(mockup.Name, policy.AuthenticationProviderId!, policy.PasswordResetProviderId!)
+ var user = new User(mockup.Name, policy.AuthenticationProviderId!, policy.PasswordResetProviderId!)
+ {
+ Id = entry.GetGuid(1),
+ InternalId = entry.GetInt64(0),
+ MaxParentalRatingScore = policy.MaxParentalRating,
+ MaxParentalRatingSubScore = null,
+ EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess,
+ RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit,
+ InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount,
+ LoginAttemptsBeforeLockout = maxLoginAttempts,
+ SubtitleMode = config.SubtitleMode,
+ HidePlayedInLatest = config.HidePlayedInLatest,
+ EnableLocalPassword = config.EnableLocalPassword,
+ PlayDefaultAudioTrack = config.PlayDefaultAudioTrack,
+ DisplayCollectionsView = config.DisplayCollectionsView,
+ DisplayMissingEpisodes = config.DisplayMissingEpisodes,
+ AudioLanguagePreference = config.AudioLanguagePreference,
+ RememberAudioSelections = config.RememberAudioSelections,
+ EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay,
+ RememberSubtitleSelections = config.RememberSubtitleSelections,
+ SubtitleLanguagePreference = config.SubtitleLanguagePreference,
+ Password = mockup.Password,
+ LastLoginDate = mockup.LastLoginDate,
+ LastActivityDate = mockup.LastActivityDate
+ };
+
+ if (mockup.ImageInfos.Length > 0)
+ {
+ ItemImageInfo info = mockup.ImageInfos[0];
+
+ user.ProfileImage = new ImageInfo(info.Path)
{
- Id = entry.GetGuid(1),
- InternalId = entry.GetInt64(0),
- MaxParentalRatingScore = policy.MaxParentalRating,
- MaxParentalRatingSubScore = null,
- EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess,
- RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit,
- InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount,
- LoginAttemptsBeforeLockout = maxLoginAttempts,
- SubtitleMode = config.SubtitleMode,
- HidePlayedInLatest = config.HidePlayedInLatest,
- EnableLocalPassword = config.EnableLocalPassword,
- PlayDefaultAudioTrack = config.PlayDefaultAudioTrack,
- DisplayCollectionsView = config.DisplayCollectionsView,
- DisplayMissingEpisodes = config.DisplayMissingEpisodes,
- AudioLanguagePreference = config.AudioLanguagePreference,
- RememberAudioSelections = config.RememberAudioSelections,
- EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay,
- RememberSubtitleSelections = config.RememberSubtitleSelections,
- SubtitleLanguagePreference = config.SubtitleLanguagePreference,
- Password = mockup.Password,
- LastLoginDate = mockup.LastLoginDate,
- LastActivityDate = mockup.LastActivityDate
+ LastModified = info.DateModified
};
+ }
- if (mockup.ImageInfos.Length > 0)
- {
- ItemImageInfo info = mockup.ImageInfos[0];
-
- user.ProfileImage = new ImageInfo(info.Path)
- {
- LastModified = info.DateModified
- };
- }
-
- user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
- user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
- user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
- user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
- user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
- user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
- user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
- user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
- user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding);
- user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding);
- user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
- user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
- user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
- user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
- user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
- user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
- user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
- user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
- user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
- user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
- user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
- user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
-
- foreach (var policyAccessSchedule in policy.AccessSchedules)
- {
- user.AccessSchedules.Add(policyAccessSchedule);
- }
-
- user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
- user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
- user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
- user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
- user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
- user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
- user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
- user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
- user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
-
- dbContext.Users.Add(user);
+ user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
+ user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
+ user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
+ user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
+ user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
+ user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
+ user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
+ user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
+ user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding);
+ user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding);
+ user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
+ user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
+ user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
+ user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
+ user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
+ user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
+ user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
+ user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
+ user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
+ user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
+ user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
+ user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
+
+ foreach (var policyAccessSchedule in policy.AccessSchedules)
+ {
+ user.AccessSchedules.Add(policyAccessSchedule);
}
- dbContext.SaveChanges();
+ user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
+ user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
+ user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
+ user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
+ user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
+ user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
+ user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
+ user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
+ user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
+
+ dbContext.Users.Add(user);
}
- try
- {
- File.Move(Path.Combine(dataPath, DbFilename), Path.Combine(dataPath, DbFilename + ".old"));
+ dbContext.SaveChanges();
+ }
- var journalPath = Path.Combine(dataPath, DbFilename + "-journal");
- if (File.Exists(journalPath))
- {
- File.Move(journalPath, Path.Combine(dataPath, DbFilename + ".old-journal"));
- }
- }
- catch (IOException e)
+ try
+ {
+ File.Move(Path.Combine(dataPath, DbFilename), Path.Combine(dataPath, DbFilename + ".old"));
+
+ var journalPath = Path.Combine(dataPath, DbFilename + "-journal");
+ if (File.Exists(journalPath))
{
- _logger.LogError(e, "Error renaming legacy user database to 'users.db.old'");
+ File.Move(journalPath, Path.Combine(dataPath, DbFilename + ".old-journal"));
}
}
+ catch (IOException e)
+ {
+ _logger.LogError(e, "Error renaming legacy user database to 'users.db.old'");
+ }
+ }
#nullable disable
- internal class UserMockup
- {
- public string Password { get; set; }
+ internal class UserMockup
+ {
+ public string Password { get; set; }
- public string EasyPassword { get; set; }
+ public string EasyPassword { get; set; }
- public DateTime? LastLoginDate { get; set; }
+ public DateTime? LastLoginDate { get; set; }
- public DateTime? LastActivityDate { get; set; }
+ public DateTime? LastActivityDate { get; set; }
- public string Name { get; set; }
+ public string Name { get; set; }
- public ItemImageInfo[] ImageInfos { get; set; }
- }
+ public ItemImageInfo[] ImageInfos { get; set; }
}
}
diff --git a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs
index 9031f2fdc..8b394dd7a 100644
--- a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs
+++ b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs
@@ -8,13 +8,15 @@ using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Server.ServerSetupApp;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.IO;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@@ -24,13 +26,11 @@ 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
+[JellyfinMigration("2025-04-20T21:00:00", nameof(MoveExtractedFiles))]
+public class MoveExtractedFiles : IAsyncMigrationRoutine
{
private readonly IApplicationPaths _appPaths;
- private readonly ILogger<MoveExtractedFiles> _logger;
+ private readonly ILogger _logger;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IPathManager _pathManager;
private readonly IFileSystem _fileSystem;
@@ -40,18 +40,20 @@ public class MoveExtractedFiles : IMigrationRoutine
/// </summary>
/// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
/// <param name="logger">The logger.</param>
+ /// <param name="startupLogger">The startup logger for Startup UI intigration.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
/// <param name="dbProvider">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
public MoveExtractedFiles(
IApplicationPaths appPaths,
ILogger<MoveExtractedFiles> logger,
+ IStartupLogger<MoveExtractedFiles> startupLogger,
IPathManager pathManager,
IFileSystem fileSystem,
IDbContextFactory<JellyfinDbContext> dbProvider)
{
_appPaths = appPaths;
- _logger = logger;
+ _logger = startupLogger.With(logger);
_pathManager = pathManager;
_fileSystem = fileSystem;
_dbProvider = dbProvider;
@@ -62,10 +64,10 @@ public class MoveExtractedFiles : IMigrationRoutine
private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments");
/// <inheritdoc />
- public void Perform()
+ public async Task PerformAsync(CancellationToken cancellationToken)
{
const int Limit = 5000;
- int itemCount = 0, offset = 0;
+ int itemCount = 0;
var sw = Stopwatch.StartNew();
@@ -76,27 +78,27 @@ public class MoveExtractedFiles : IMigrationRoutine
// Make sure directories exist
Directory.CreateDirectory(SubtitleCachePath);
Directory.CreateDirectory(AttachmentCachePath);
- do
+
+ await foreach (var result in context.BaseItems
+ .Include(e => e.MediaStreams!.Where(s => s.StreamType == MediaStreamTypeEntity.Subtitle && !s.IsExternal))
+ .Where(b => b.MediaType == MediaType.Video.ToString() && !b.IsVirtualItem && !b.IsFolder)
+ .Select(b => new
+ {
+ b.Id,
+ b.Path,
+ b.MediaStreams
+ })
+ .OrderBy(e => e.Id)
+ .WithPartitionProgress((partition) => _logger.LogInformation("Checked: {Count} - Moved: {Items} - Time: {Time}", partition * Limit, itemCount, sw.Elapsed))
+ .PartitionEagerAsync(Limit, cancellationToken)
+ .WithCancellation(cancellationToken)
+ .ConfigureAwait(false))
{
- var results = context.BaseItems
- .Include(e => e.MediaStreams!.Where(s => s.StreamType == MediaStreamTypeEntity.Subtitle && !s.IsExternal))
- .Where(b => b.MediaType == MediaType.Video.ToString() && !b.IsVirtualItem && !b.IsFolder)
- .OrderBy(e => e.Id)
- .Skip(offset)
- .Take(Limit)
- .Select(b => new Tuple<Guid, string?, ICollection<MediaStreamInfo>?>(b.Id, b.Path, b.MediaStreams)).ToList();
-
- foreach (var result in results)
+ if (MoveSubtitleAndAttachmentFiles(result.Id, result.Path, result.MediaStreams, context))
{
- if (MoveSubtitleAndAttachmentFiles(result.Item1, result.Item2, result.Item3, context))
- {
- itemCount++;
- }
+ itemCount++;
}
-
- offset += Limit;
- _logger.LogInformation("Checked: {Count} - Moved: {Items} - Time: {Time}", offset, itemCount, sw.Elapsed);
- } while (offset < records);
+ }
_logger.LogInformation("Moved files for {Count} items in {Time}", itemCount, sw.Elapsed);
diff --git a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
index 607708043..0f55465e8 100644
--- a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
+++ b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
@@ -4,6 +4,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using Jellyfin.Data.Enums;
+using Jellyfin.Server.ServerSetupApp;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Trickplay;
@@ -15,15 +16,15 @@ 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
+[JellyfinMigration("2025-04-20T23:00:00", nameof(MoveTrickplayFiles), RunMigrationOnSetup = true)]
public class MoveTrickplayFiles : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
private readonly ITrickplayManager _trickplayManager;
private readonly IFileSystem _fileSystem;
private readonly ILibraryManager _libraryManager;
- private readonly ILogger<MoveTrickplayFiles> _logger;
+ private readonly IStartupLogger _logger;
/// <summary>
/// Initializes a new instance of the <see cref="MoveTrickplayFiles"/> class.
@@ -36,7 +37,7 @@ public class MoveTrickplayFiles : IMigrationRoutine
ITrickplayManager trickplayManager,
IFileSystem fileSystem,
ILibraryManager libraryManager,
- ILogger<MoveTrickplayFiles> logger)
+ IStartupLogger<MoveTrickplayFiles> logger)
{
_trickplayManager = trickplayManager;
_fileSystem = fileSystem;
diff --git a/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs
index 1ef1dd45f..ebf4a2780 100644
--- a/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs
+++ b/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs
@@ -2,42 +2,41 @@ using System;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Updates;
-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)]
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Migration to initialize system configuration with the default plugin repository.
+/// </summary>
#pragma warning disable CS0618 // Type or member is obsolete
- public class ReaddDefaultPluginRepository : IMigrationRoutine
+[JellyfinMigration("2025-04-20T11:00:00", nameof(ReaddDefaultPluginRepository), "5F86E7F6-D966-4C77-849D-7A7B40B68C4E", RunMigrationOnSetup = true)]
+public class ReaddDefaultPluginRepository : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
- {
- private readonly IServerConfigurationManager _serverConfigurationManager;
+{
+ private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly RepositoryInfo _defaultRepositoryInfo = new RepositoryInfo
- {
- Name = "Jellyfin Stable",
- Url = "https://repo.jellyfin.org/releases/plugin/manifest-stable.json"
- };
+ private readonly RepositoryInfo _defaultRepositoryInfo = new RepositoryInfo
+ {
+ Name = "Jellyfin Stable",
+ Url = "https://repo.jellyfin.org/releases/plugin/manifest-stable.json"
+ };
- /// <summary>
- /// Initializes a new instance of the <see cref="ReaddDefaultPluginRepository"/> class.
- /// </summary>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- public ReaddDefaultPluginRepository(IServerConfigurationManager serverConfigurationManager)
- {
- _serverConfigurationManager = serverConfigurationManager;
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ReaddDefaultPluginRepository"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public ReaddDefaultPluginRepository(IServerConfigurationManager serverConfigurationManager)
+ {
+ _serverConfigurationManager = serverConfigurationManager;
+ }
- /// <inheritdoc/>
- public void Perform()
+ /// <inheritdoc/>
+ public void Perform()
+ {
+ // Only add if repository list is empty
+ if (_serverConfigurationManager.Configuration.PluginRepositories.Length == 0)
{
- // Only add if repository list is empty
- if (_serverConfigurationManager.Configuration.PluginRepositories.Length == 0)
- {
- _serverConfigurationManager.Configuration.PluginRepositories = new[] { _defaultRepositoryInfo };
- _serverConfigurationManager.SaveConfiguration();
- }
+ _serverConfigurationManager.Configuration.PluginRepositories = new[] { _defaultRepositoryInfo };
+ _serverConfigurationManager.SaveConfiguration();
}
}
}
diff --git a/Jellyfin.Server/Migrations/Routines/RefreshInternalDateModified.cs b/Jellyfin.Server/Migrations/Routines/RefreshInternalDateModified.cs
new file mode 100644
index 000000000..b23a7dbc4
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/RefreshInternalDateModified.cs
@@ -0,0 +1,131 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Extensions;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.IO;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Migration to re-read creation dates for library items with internal metadata paths.
+/// </summary>
+[JellyfinMigration("2025-04-20T23:00:00", nameof(RefreshInternalDateModified))]
+public class RefreshInternalDateModified : IDatabaseMigrationRoutine
+{
+ private readonly ILogger<RefreshInternalDateModified> _logger;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+ private readonly IFileSystem _fileSystem;
+ private readonly IServerApplicationHost _applicationHost;
+ private readonly bool _useFileCreationTimeForDateAdded;
+
+ private IReadOnlyList<string> _internalTypes = [
+ typeof(Genre).FullName!,
+ typeof(MusicGenre).FullName!,
+ typeof(MusicArtist).FullName!,
+ typeof(People).FullName!,
+ typeof(Studio).FullName!
+ ];
+
+ private IReadOnlyList<string> _internalPaths;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RefreshInternalDateModified"/> class.
+ /// </summary>
+ /// <param name="applicationHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
+ /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
+ /// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="dbProvider">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ public RefreshInternalDateModified(
+ IServerApplicationHost applicationHost,
+ IServerApplicationPaths applicationPaths,
+ IServerConfigurationManager configurationManager,
+ IDbContextFactory<JellyfinDbContext> dbProvider,
+ ILogger<RefreshInternalDateModified> logger,
+ IFileSystem fileSystem)
+ {
+ _dbProvider = dbProvider;
+ _logger = logger;
+ _fileSystem = fileSystem;
+ _applicationHost = applicationHost;
+ _internalPaths = [
+ applicationPaths.ArtistsPath,
+ applicationPaths.GenrePath,
+ applicationPaths.MusicGenrePath,
+ applicationPaths.StudioPath,
+ applicationPaths.PeoplePath
+ ];
+ _useFileCreationTimeForDateAdded = configurationManager.GetMetadataConfiguration().UseFileCreationTimeForDateAdded;
+ }
+
+ /// <inheritdoc />
+ public void Perform()
+ {
+ const int Limit = 5000;
+ int itemCount = 0, offset = 0;
+
+ var sw = Stopwatch.StartNew();
+
+ using var context = _dbProvider.CreateDbContext();
+ var records = context.BaseItems.Count(b => _internalTypes.Contains(b.Type));
+ _logger.LogInformation("Checking if {Count} potentially internal items require refreshed DateModified", records);
+
+ do
+ {
+ var results = context.BaseItems
+ .Where(b => _internalTypes.Contains(b.Type))
+ .OrderBy(e => e.Id)
+ .Skip(offset)
+ .Take(Limit)
+ .ToList();
+
+ foreach (var item in results)
+ {
+ var itemPath = item.Path;
+ if (itemPath is not null)
+ {
+ var realPath = _applicationHost.ExpandVirtualPath(item.Path);
+ if (_internalPaths.Any(path => realPath.StartsWith(path, StringComparison.Ordinal)))
+ {
+ var writeTime = _fileSystem.GetLastWriteTimeUtc(realPath);
+ var itemModificationTime = item.DateModified;
+ if (writeTime != itemModificationTime)
+ {
+ _logger.LogDebug("Reset file modification date: Old: {Old} - New: {New} - Path: {Path}", itemModificationTime, writeTime, realPath);
+ item.DateModified = writeTime;
+ if (_useFileCreationTimeForDateAdded)
+ {
+ item.DateCreated = _fileSystem.GetCreationTimeUtc(realPath);
+ }
+
+ itemCount++;
+ }
+ }
+ }
+ }
+
+ offset += Limit;
+ if (offset > records)
+ {
+ offset = records;
+ }
+
+ _logger.LogInformation("Checked: {Count} - Refreshed: {Items} - Time: {Time}", offset, itemCount, sw.Elapsed);
+ } while (offset < records);
+
+ context.SaveChanges();
+
+ _logger.LogInformation("Refreshed DateModified for {Count} items in {Time}", itemCount, sw.Elapsed);
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs b/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs
index 477363e0d..b626c473e 100644
--- a/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs
+++ b/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs
@@ -3,44 +3,43 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using Microsoft.Extensions.Logging;
-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")]
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Removes the old 'RemoveDownloadImagesInAdvance' from library options.
+/// </summary>
#pragma warning disable CS0618 // Type or member is obsolete
- internal class RemoveDownloadImagesInAdvance : IMigrationRoutine
+[JellyfinMigration("2025-04-20T13:00:00", nameof(RemoveDownloadImagesInAdvance), "A81F75E0-8F43-416F-A5E8-516CCAB4D8CC")]
+internal class RemoveDownloadImagesInAdvance : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
- {
- private readonly ILogger<RemoveDownloadImagesInAdvance> _logger;
- private readonly ILibraryManager _libraryManager;
+{
+ private readonly ILogger<RemoveDownloadImagesInAdvance> _logger;
+ private readonly ILibraryManager _libraryManager;
- public RemoveDownloadImagesInAdvance(ILogger<RemoveDownloadImagesInAdvance> logger, ILibraryManager libraryManager)
- {
- _logger = logger;
- _libraryManager = libraryManager;
- }
+ public RemoveDownloadImagesInAdvance(ILogger<RemoveDownloadImagesInAdvance> logger, ILibraryManager libraryManager)
+ {
+ _logger = logger;
+ _libraryManager = libraryManager;
+ }
- /// <inheritdoc/>
- public void Perform()
+ /// <inheritdoc/>
+ public void Perform()
+ {
+ var virtualFolders = _libraryManager.GetVirtualFolders(false);
+ _logger.LogInformation("Removing 'RemoveDownloadImagesInAdvance' settings in all the libraries");
+ foreach (var virtualFolder in virtualFolders)
{
- var virtualFolders = _libraryManager.GetVirtualFolders(false);
- _logger.LogInformation("Removing 'RemoveDownloadImagesInAdvance' settings in all the libraries");
- foreach (var virtualFolder in virtualFolders)
+ // Some virtual folders don't have a proper item id.
+ if (!Guid.TryParse(virtualFolder.ItemId, out var folderId))
{
- // Some virtual folders don't have a proper item id.
- if (!Guid.TryParse(virtualFolder.ItemId, out var folderId))
- {
- continue;
- }
-
- var libraryOptions = virtualFolder.LibraryOptions;
- var collectionFolder = _libraryManager.GetItemById<CollectionFolder>(folderId) ?? throw new InvalidOperationException("Failed to find CollectionFolder");
- // The property no longer exists in LibraryOptions, so we just re-save the options to get old data removed.
- collectionFolder.UpdateLibraryOptions(libraryOptions);
- _logger.LogInformation("Removed from '{VirtualFolder}'", virtualFolder.Name);
+ continue;
}
+
+ var libraryOptions = virtualFolder.LibraryOptions;
+ var collectionFolder = _libraryManager.GetItemById<CollectionFolder>(folderId) ?? throw new InvalidOperationException("Failed to find CollectionFolder");
+ // The property no longer exists in LibraryOptions, so we just re-save the options to get old data removed.
+ collectionFolder.UpdateLibraryOptions(libraryOptions);
+ _logger.LogInformation("Removed from '{VirtualFolder}'", virtualFolder.Name);
}
}
}
diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
index c80512dee..c9e66d0cf 100644
--- a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
+++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
@@ -7,71 +7,70 @@ using MediaBrowser.Controller;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
-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")]
+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>
#pragma warning disable CS0618 // Type or member is obsolete
- internal class RemoveDuplicateExtras : IMigrationRoutine
+[JellyfinMigration("2025-04-20T08:00:00", nameof(RemoveDuplicateExtras), "ACBE17B7-8435-4A83-8B64-6FCF162CB9BD")]
+internal class RemoveDuplicateExtras : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
+{
+ private const string DbFilename = "library.db";
+ private readonly ILogger<RemoveDuplicateExtras> _logger;
+ private readonly IServerApplicationPaths _paths;
+
+ public RemoveDuplicateExtras(ILogger<RemoveDuplicateExtras> logger, IServerApplicationPaths paths)
{
- private const string DbFilename = "library.db";
- private readonly ILogger<RemoveDuplicateExtras> _logger;
- private readonly IServerApplicationPaths _paths;
+ _logger = logger;
+ _paths = paths;
+ }
- public RemoveDuplicateExtras(ILogger<RemoveDuplicateExtras> logger, IServerApplicationPaths paths)
+ /// <inheritdoc/>
+ public void Perform()
+ {
+ var dataPath = _paths.DataPath;
+ var dbPath = Path.Combine(dataPath, DbFilename);
+ using var connection = new SqliteConnection($"Filename={dbPath}");
+ connection.Open();
+ using (var transaction = connection.BeginTransaction())
{
- _logger = logger;
- _paths = paths;
- }
+ // Query the database for the ids of duplicate extras
+ var queryResult = connection.Query("SELECT t1.Path FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video'");
+ var bads = string.Join(", ", queryResult.Select(x => x.GetString(0)));
- /// <inheritdoc/>
- public void Perform()
- {
- var dataPath = _paths.DataPath;
- var dbPath = Path.Combine(dataPath, DbFilename);
- using var connection = new SqliteConnection($"Filename={dbPath}");
- connection.Open();
- using (var transaction = connection.BeginTransaction())
+ // Do nothing if no duplicate extras were detected
+ if (bads.Length == 0)
{
- // Query the database for the ids of duplicate extras
- var queryResult = connection.Query("SELECT t1.Path FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video'");
- var bads = string.Join(", ", queryResult.Select(x => x.GetString(0)));
-
- // Do nothing if no duplicate extras were detected
- if (bads.Length == 0)
- {
- _logger.LogInformation("No duplicate extras detected, skipping migration.");
- return;
- }
+ _logger.LogInformation("No duplicate extras detected, skipping migration.");
+ return;
+ }
- // Back up the database before deleting any entries
- for (int i = 1; ; i++)
+ // Back up the database before deleting any entries
+ for (int i = 1; ; i++)
+ {
+ var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i);
+ if (!File.Exists(bakPath))
{
- var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i);
- if (!File.Exists(bakPath))
+ try
{
- try
- {
- File.Copy(dbPath, bakPath);
- _logger.LogInformation("Library database backed up to {BackupPath}", bakPath);
- break;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath);
- throw;
- }
+ File.Copy(dbPath, bakPath);
+ _logger.LogInformation("Library database backed up to {BackupPath}", bakPath);
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath);
+ throw;
}
}
-
- // Delete all duplicate extras
- _logger.LogInformation("Removing found duplicated extras for the following items: {DuplicateExtras}", bads);
- connection.Execute("DELETE FROM TypedBaseItems WHERE rowid IN (SELECT t1.rowid FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video')");
- transaction.Commit();
}
+
+ // Delete all duplicate extras
+ _logger.LogInformation("Removing found duplicated extras for the following items: {DuplicateExtras}", bads);
+ connection.Execute("DELETE FROM TypedBaseItems WHERE rowid IN (SELECT t1.rowid FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video')");
+ transaction.Commit();
}
}
}
diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs
index ce2be2755..23f212424 100644
--- a/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs
+++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs
@@ -11,8 +11,8 @@ 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
+[JellyfinMigration("2025-04-20T19:00:00", nameof(RemoveDuplicatePlaylistChildren), "96C156A2-7A13-4B3B-A8B8-FB80C94D20C0")]
internal class RemoveDuplicatePlaylistChildren : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs b/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs
new file mode 100644
index 000000000..502763ac0
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs
@@ -0,0 +1,74 @@
+#pragma warning disable RS0030 // Do not use banned APIs
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Server.Implementations.Data;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Server.ServerSetupApp;
+using MediaBrowser.Controller;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+[JellyfinMigration("2025-07-30T21:50:00", nameof(ReseedFolderFlag))]
+[JellyfinMigrationBackup(JellyfinDb = true)]
+internal class ReseedFolderFlag : IAsyncMigrationRoutine
+{
+ private const string DbFilename = "library.db.old";
+
+ private readonly IStartupLogger _logger;
+ private readonly IServerApplicationPaths _paths;
+ private readonly IDbContextFactory<JellyfinDbContext> _provider;
+
+ public ReseedFolderFlag(
+ IStartupLogger<MigrateLibraryDb> startupLogger,
+ IDbContextFactory<JellyfinDbContext> provider,
+ IServerApplicationPaths paths)
+ {
+ _logger = startupLogger;
+ _provider = provider;
+ _paths = paths;
+ }
+
+ internal static bool RerunGuardFlag { get; set; } = false;
+
+ public async Task PerformAsync(CancellationToken cancellationToken)
+ {
+ if (RerunGuardFlag)
+ {
+ _logger.LogInformation("Migration is skipped because it does not apply.");
+ return;
+ }
+
+ _logger.LogInformation("Migrating the IsFolder flag from library.db.old may take a while, do not stop Jellyfin.");
+
+ var dataPath = _paths.DataPath;
+ var libraryDbPath = Path.Combine(dataPath, DbFilename);
+ if (!File.Exists(libraryDbPath))
+ {
+ _logger.LogError("Cannot migrate IsFolder flag from {LibraryDb} as it does not exist. This migration expects the MigrateLibraryDb to run first.", libraryDbPath);
+ return;
+ }
+
+ var dbContext = await _provider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly");
+ var queryResult = connection.Query(
+ """
+ SELECT guid FROM TypedBaseItems
+ WHERE IsFolder = true
+ """)
+ .Select(entity => entity.GetGuid(0))
+ .ToList();
+ _logger.LogInformation("Migrating the IsFolder flag for {Count} items.", queryResult.Count);
+ foreach (var id in queryResult)
+ {
+ await dbContext.BaseItems.Where(e => e.Id == id).ExecuteUpdateAsync(e => e.SetProperty(f => f.IsFolder, true), cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs
index cf3f5433b..f58cf2741 100644
--- a/Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs
+++ b/Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs
@@ -6,8 +6,8 @@ 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
+[JellyfinMigration("2025-04-20T17:00:00", nameof(UpdateDefaultPluginRepository), "852816E0-2712-49A9-9240-C6FC5FCAD1A8", RunMigrationOnSetup = true)]
public class UpdateDefaultPluginRepository : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
{
diff --git a/Jellyfin.Server/Migrations/Stages/CodeMigration.cs b/Jellyfin.Server/Migrations/Stages/CodeMigration.cs
index 1e4dfb237..264710bce 100644
--- a/Jellyfin.Server/Migrations/Stages/CodeMigration.cs
+++ b/Jellyfin.Server/Migrations/Stages/CodeMigration.cs
@@ -2,22 +2,53 @@ using System;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Server.ServerSetupApp;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.Stages;
-internal class CodeMigration(Type migrationType, JellyfinMigrationAttribute metadata)
+internal class CodeMigration(Type migrationType, JellyfinMigrationAttribute metadata, JellyfinMigrationBackupAttribute? migrationBackupAttribute)
{
public Type MigrationType { get; } = migrationType;
public JellyfinMigrationAttribute Metadata { get; } = metadata;
+ public JellyfinMigrationBackupAttribute? BackupRequirements { get; set; } = migrationBackupAttribute;
+
public string BuildCodeMigrationId()
{
- return Metadata.Order.ToString("yyyyMMddHHmmsss", CultureInfo.InvariantCulture) + "_" + MigrationType.Name!;
+ return Metadata.Order.ToString("yyyyMMddHHmmsss", CultureInfo.InvariantCulture) + "_" + Metadata.Name!;
+ }
+
+ private IServiceCollection MigrationServices(IServiceProvider serviceProvider, IStartupLogger logger)
+ {
+ var childServiceCollection = new ServiceCollection()
+ .AddSingleton(serviceProvider)
+ .AddSingleton(logger)
+ .AddSingleton(typeof(IStartupLogger<>), typeof(NestedStartupLogger<>))
+ .AddSingleton<StartupLogTopic>(logger.Topic!);
+
+ foreach (ServiceDescriptor service in serviceProvider.GetRequiredService<IServiceCollection>())
+ {
+ if (service.Lifetime == ServiceLifetime.Singleton && !service.ServiceType.IsGenericTypeDefinition)
+ {
+ object? serviceInstance = serviceProvider.GetService(service.ServiceType);
+ if (serviceInstance != null)
+ {
+ childServiceCollection.AddSingleton(service.ServiceType, serviceInstance);
+ continue;
+ }
+ }
+
+ childServiceCollection.Add(service);
+ }
+
+ return childServiceCollection;
}
- public async Task Perform(IServiceProvider? serviceProvider, CancellationToken cancellationToken)
+ public async Task Perform(IServiceProvider? serviceProvider, IStartupLogger logger, CancellationToken cancellationToken)
{
#pragma warning disable CS0618 // Type or member is obsolete
if (typeof(IMigrationRoutine).IsAssignableFrom(MigrationType))
@@ -28,7 +59,8 @@ internal class CodeMigration(Type migrationType, JellyfinMigrationAttribute meta
}
else
{
- ((IMigrationRoutine)ActivatorUtilities.CreateInstance(serviceProvider, MigrationType)).Perform();
+ using var migrationServices = MigrationServices(serviceProvider, logger).BuildServiceProvider();
+ ((IMigrationRoutine)ActivatorUtilities.CreateInstance(migrationServices, MigrationType)).Perform();
#pragma warning restore CS0618 // Type or member is obsolete
}
}
@@ -40,7 +72,8 @@ internal class CodeMigration(Type migrationType, JellyfinMigrationAttribute meta
}
else
{
- await ((IAsyncMigrationRoutine)ActivatorUtilities.CreateInstance(serviceProvider, MigrationType)).PerformAsync(cancellationToken).ConfigureAwait(false);
+ using var migrationServices = MigrationServices(serviceProvider, logger).BuildServiceProvider();
+ await ((IAsyncMigrationRoutine)ActivatorUtilities.CreateInstance(migrationServices, MigrationType)).PerformAsync(cancellationToken).ConfigureAwait(false);
}
}
else
@@ -48,4 +81,11 @@ internal class CodeMigration(Type migrationType, JellyfinMigrationAttribute meta
throw new InvalidOperationException($"The type {MigrationType} does not implement either IMigrationRoutine or IAsyncMigrationRoutine and is not a valid migration type");
}
}
+
+ private class NestedStartupLogger<TCategory> : StartupLogger<TCategory>
+ {
+ public NestedStartupLogger(ILogger logger, StartupLogTopic topic) : base(logger, topic)
+ {
+ }
+ }
}
diff --git a/Jellyfin.Server/Migrations/Stages/JellyfinMigrationStageTypes.cs b/Jellyfin.Server/Migrations/Stages/JellyfinMigrationStageTypes.cs
index d90ad3d9b..3d5ec233b 100644
--- a/Jellyfin.Server/Migrations/Stages/JellyfinMigrationStageTypes.cs
+++ b/Jellyfin.Server/Migrations/Stages/JellyfinMigrationStageTypes.cs
@@ -17,7 +17,7 @@ public enum JellyfinMigrationStageTypes
/// 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,
+ CoreInitialisation = 2,
/// <summary>
/// Runs after services has been registered and initialised. Last step before running the server.