Library.db migration impovements (#13809)

* Fixes cleanup of wrong table in migration

* use dedicated context for each step

* Use prepared Context

* Fix measurement of UserData migration time

* Update logging and combine cleanup to its own stage

* fix people map not logging
migrate only readonly database

* Add id blacklisting in migration to avoid duplicated log entires
pull/12615/head^2
JPVenson 3 weeks ago committed by GitHub
parent 476a0d6932
commit 90a6cca92b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -73,273 +73,328 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
var dataPath = _paths.DataPath;
var libraryDbPath = Path.Combine(dataPath, DbFilename);
using var connection = new SqliteConnection($"Filename={libraryDbPath}");
var migrationTotalTime = TimeSpan.Zero;
using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly");
var stopwatch = new Stopwatch();
stopwatch.Start();
var fullOperationTimer = new Stopwatch();
fullOperationTimer.Start();
connection.Open();
using var dbContext = _provider.CreateDbContext();
migrationTotalTime += stopwatch.Elapsed;
_logger.LogInformation("Saving UserData entries took {0}.", stopwatch.Elapsed);
stopwatch.Restart();
_logger.LogInformation("Start moving TypedBaseItem.");
const string typedBaseItemsQuery = """
SELECT guid, type, data, StartDate, EndDate, ChannelId, IsMovie,
IsSeries, EpisodeTitle, IsRepeat, CommunityRating, CustomRating, IndexNumber, IsLocked, PreferredMetadataLanguage,
PreferredMetadataCountryCode, Width, Height, DateLastRefreshed, Name, Path, PremiereDate, Overview, ParentIndexNumber,
ProductionYear, OfficialRating, ForcedSortName, RunTimeTicks, Size, DateCreated, DateModified, Genres, ParentId, TopParentId,
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
""";
dbContext.BaseItems.ExecuteDelete();
using (var operation = GetPreparedDbContext("Cleanup database"))
{
operation.JellyfinDbContext.BaseItems.ExecuteDelete();
operation.JellyfinDbContext.ItemValues.ExecuteDelete();
operation.JellyfinDbContext.UserData.ExecuteDelete();
operation.JellyfinDbContext.MediaStreamInfos.ExecuteDelete();
operation.JellyfinDbContext.Peoples.ExecuteDelete();
operation.JellyfinDbContext.PeopleBaseItemMap.ExecuteDelete();
operation.JellyfinDbContext.Chapters.ExecuteDelete();
operation.JellyfinDbContext.AncestorIds.ExecuteDelete();
}
var legacyBaseItemWithUserKeys = new Dictionary<string, BaseItemEntity>();
foreach (SqliteDataReader dto in connection.Query(typedBaseItemsQuery))
{
var baseItem = GetItem(dto);
dbContext.BaseItems.Add(baseItem.BaseItem);
foreach (var dataKey in baseItem.LegacyUserDataKey)
connection.Open();
var baseItemIds = new HashSet<Guid>();
using (var operation = GetPreparedDbContext("moving TypedBaseItem"))
{
const string typedBaseItemsQuery =
"""
SELECT guid, type, data, StartDate, EndDate, ChannelId, IsMovie,
IsSeries, EpisodeTitle, IsRepeat, CommunityRating, CustomRating, IndexNumber, IsLocked, PreferredMetadataLanguage,
PreferredMetadataCountryCode, Width, Height, DateLastRefreshed, Name, Path, PremiereDate, Overview, ParentIndexNumber,
ProductionYear, OfficialRating, ForcedSortName, RunTimeTicks, Size, DateCreated, DateModified, Genres, ParentId, TopParentId,
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
""";
using (new TrackedMigrationStep("Loading TypedBaseItems", _logger))
{
foreach (SqliteDataReader dto in connection.Query(typedBaseItemsQuery))
{
var baseItem = GetItem(dto);
operation.JellyfinDbContext.BaseItems.Add(baseItem.BaseItem);
baseItemIds.Add(baseItem.BaseItem.Id);
foreach (var dataKey in baseItem.LegacyUserDataKey)
{
legacyBaseItemWithUserKeys[dataKey] = baseItem.BaseItem;
}
}
}
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.BaseItems.Local.Count} BaseItem entries", _logger))
{
legacyBaseItemWithUserKeys[dataKey] = baseItem.BaseItem;
operation.JellyfinDbContext.SaveChanges();
}
}
_logger.LogInformation("Try saving {0} BaseItem entries.", dbContext.BaseItems.Local.Count);
dbContext.SaveChanges();
migrationTotalTime += stopwatch.Elapsed;
_logger.LogInformation("Saving BaseItems entries took {0}.", stopwatch.Elapsed);
stopwatch.Restart();
using (var operation = GetPreparedDbContext("moving ItemValues"))
{
// do not migrate inherited types as they are now properly mapped in search and lookup.
const string itemValueQuery =
"""
SELECT ItemId, Type, Value, CleanValue FROM ItemValues
WHERE Type <> 6 AND EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = ItemValues.ItemId)
""";
_logger.LogInformation("Start moving ItemValues.");
// do not migrate inherited types as they are now properly mapped in search and lookup.
const string itemValueQuery =
"""
SELECT ItemId, Type, Value, CleanValue FROM ItemValues
WHERE Type <> 6 AND EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = ItemValues.ItemId)
""";
dbContext.ItemValues.ExecuteDelete();
// EFCores local lookup sucks. We cannot use context.ItemValues.Local here because its just super slow.
var localItems = new Dictionary<(int Type, string CleanValue), (Database.Implementations.Entities.ItemValue ItemValue, List<Guid> ItemIds)>();
using (new TrackedMigrationStep("loading ItemValues", _logger))
{
foreach (SqliteDataReader dto in connection.Query(itemValueQuery))
{
var itemId = dto.GetGuid(0);
var entity = GetItemValue(dto);
var key = ((int)entity.Type, entity.CleanValue);
if (!localItems.TryGetValue(key, out var existing))
{
localItems[key] = existing = (entity, []);
}
// EFCores local lookup sucks. We cannot use context.ItemValues.Local here because its just super slow.
var localItems = new Dictionary<(int Type, string CleanValue), (Database.Implementations.Entities.ItemValue ItemValue, List<Guid> ItemIds)>();
existing.ItemIds.Add(itemId);
}
foreach (SqliteDataReader dto in connection.Query(itemValueQuery))
{
var itemId = dto.GetGuid(0);
var entity = GetItemValue(dto);
var key = ((int)entity.Type, entity.CleanValue);
if (!localItems.TryGetValue(key, out var existing))
{
localItems[key] = existing = (entity, []);
foreach (var item in localItems)
{
operation.JellyfinDbContext.ItemValues.Add(item.Value.ItemValue);
operation.JellyfinDbContext.ItemValuesMap.AddRange(item.Value.ItemIds.Distinct().Select(f => new ItemValueMap()
{
Item = null!,
ItemValue = null!,
ItemId = f,
ItemValueId = item.Value.ItemValue.ItemValueId
}));
}
}
existing.ItemIds.Add(itemId);
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.ItemValues.Local.Count} ItemValues entries", _logger))
{
operation.JellyfinDbContext.SaveChanges();
}
}
foreach (var item in localItems)
using (var operation = GetPreparedDbContext("moving UserData"))
{
dbContext.ItemValues.Add(item.Value.ItemValue);
dbContext.ItemValuesMap.AddRange(item.Value.ItemIds.Distinct().Select(f => new ItemValueMap()
var queryResult = connection.Query(
"""
SELECT key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex FROM UserDatas
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.UserDataKey = UserDatas.key)
""");
using (new TrackedMigrationStep("loading UserData", _logger))
{
Item = null!,
ItemValue = null!,
ItemId = f,
ItemValueId = item.Value.ItemValue.ItemValueId
}));
}
var users = operation.JellyfinDbContext.Users.AsNoTracking().ToImmutableArray();
var userIdBlacklist = new HashSet<int>();
_logger.LogInformation("Try saving {0} ItemValues entries.", dbContext.ItemValues.Local.Count);
dbContext.SaveChanges();
migrationTotalTime += stopwatch.Elapsed;
_logger.LogInformation("Saving People ItemValues took {0}.", stopwatch.Elapsed);
stopwatch.Restart();
foreach (var entity in queryResult)
{
var userData = GetUserData(users, entity, userIdBlacklist);
if (userData is null)
{
var userDataId = entity.GetString(0);
var internalUserId = entity.GetInt32(1);
_logger.LogInformation("Start moving UserData.");
var queryResult = connection.Query("""
SELECT key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex FROM UserDatas
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);
}
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.UserDataKey = UserDatas.key)
""");
continue;
}
dbContext.UserData.ExecuteDelete();
if (!legacyBaseItemWithUserKeys.TryGetValue(userData.CustomDataKey!, out var refItem))
{
_logger.LogError("Was not able to migrate user data with key {0} because it does not reference a valid BaseItem.", entity.GetString(0));
continue;
}
var users = dbContext.Users.AsNoTracking().ToImmutableArray();
userData.ItemId = refItem.Id;
operation.JellyfinDbContext.UserData.Add(userData);
}
foreach (var entity in queryResult)
{
var userData = GetUserData(users, entity);
if (userData is null)
{
_logger.LogError("Was not able to migrate user data with key {0}", entity.GetString(0));
continue;
users.Clear();
}
if (!legacyBaseItemWithUserKeys.TryGetValue(userData.CustomDataKey!, out var refItem))
legacyBaseItemWithUserKeys.Clear();
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.UserData.Local.Count} UserData entries", _logger))
{
_logger.LogError("Was not able to migrate user data with key {0} because it does not reference a valid BaseItem.", entity.GetString(0));
continue;
operation.JellyfinDbContext.SaveChanges();
}
userData.ItemId = refItem.Id;
dbContext.UserData.Add(userData);
}
users.Clear();
legacyBaseItemWithUserKeys.Clear();
_logger.LogInformation("Try saving {0} UserData entries.", dbContext.UserData.Local.Count);
dbContext.SaveChanges();
_logger.LogInformation("Start moving MediaStreamInfos.");
const string mediaStreamQuery = """
SELECT ItemId, StreamIndex, StreamType, Codec, Language, ChannelLayout, Profile, AspectRatio, Path,
IsInterlaced, BitRate, Channels, SampleRate, IsDefault, IsForced, IsExternal, Height, Width,
AverageFrameRate, RealFrameRate, Level, PixelFormat, BitDepth, IsAnamorphic, RefFrames, CodecTag,
Comment, NalLengthSize, IsAvc, Title, TimeBase, CodecTimeBase, ColorPrimaries, ColorSpace, ColorTransfer,
DvVersionMajor, DvVersionMinor, DvProfile, DvLevel, RpuPresentFlag, ElPresentFlag, BlPresentFlag, DvBlSignalCompatibilityId, IsHearingImpaired
FROM MediaStreams
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = MediaStreams.ItemId)
""";
dbContext.MediaStreamInfos.ExecuteDelete();
foreach (SqliteDataReader dto in connection.Query(mediaStreamQuery))
using (var operation = GetPreparedDbContext("moving MediaStreamInfos"))
{
dbContext.MediaStreamInfos.Add(GetMediaStream(dto));
}
const string mediaStreamQuery =
"""
SELECT ItemId, StreamIndex, StreamType, Codec, Language, ChannelLayout, Profile, AspectRatio, Path,
IsInterlaced, BitRate, Channels, SampleRate, IsDefault, IsForced, IsExternal, Height, Width,
AverageFrameRate, RealFrameRate, Level, PixelFormat, BitDepth, IsAnamorphic, RefFrames, CodecTag,
Comment, NalLengthSize, IsAvc, Title, TimeBase, CodecTimeBase, ColorPrimaries, ColorSpace, ColorTransfer,
DvVersionMajor, DvVersionMinor, DvProfile, DvLevel, RpuPresentFlag, ElPresentFlag, BlPresentFlag, DvBlSignalCompatibilityId, IsHearingImpaired
FROM MediaStreams
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = MediaStreams.ItemId)
""";
_logger.LogInformation("Try saving {0} MediaStreamInfos entries.", dbContext.MediaStreamInfos.Local.Count);
dbContext.SaveChanges();
migrationTotalTime += stopwatch.Elapsed;
_logger.LogInformation("Saving MediaStreamInfos entries took {0}.", stopwatch.Elapsed);
stopwatch.Restart();
_logger.LogInformation("Start moving People.");
const string personsQuery = """
SELECT ItemId, Name, Role, PersonType, SortOrder FROM People
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = People.ItemId)
""";
dbContext.Peoples.ExecuteDelete();
dbContext.PeopleBaseItemMap.ExecuteDelete();
var peopleCache = new Dictionary<string, (People Person, List<PeopleBaseItemMap> Items)>();
var baseItemIds = dbContext.BaseItems.Select(b => b.Id).ToHashSet();
foreach (SqliteDataReader reader in connection.Query(personsQuery))
{
var itemId = reader.GetGuid(0);
if (!baseItemIds.Contains(itemId))
using (new TrackedMigrationStep("loading MediaStreamInfos", _logger))
{
_logger.LogError("Dont save person {0} because its not in use by any BaseItem", reader.GetString(1));
continue;
foreach (SqliteDataReader dto in connection.Query(mediaStreamQuery))
{
operation.JellyfinDbContext.MediaStreamInfos.Add(GetMediaStream(dto));
}
}
var entity = GetPerson(reader);
if (!peopleCache.TryGetValue(entity.Name, out var personCache))
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.MediaStreamInfos.Local.Count} MediaStreamInfos entries", _logger))
{
peopleCache[entity.Name] = personCache = (entity, []);
operation.JellyfinDbContext.SaveChanges();
}
}
if (reader.TryGetString(2, out var role))
{
}
using (var operation = GetPreparedDbContext("moving People"))
{
const string personsQuery =
"""
SELECT ItemId, Name, Role, PersonType, SortOrder FROM People
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = People.ItemId)
""";
int? sortOrder = reader.IsDBNull(4) ? null : reader.GetInt32(4);
var peopleCache = new Dictionary<string, (People Person, List<PeopleBaseItemMap> Items)>();
personCache.Items.Add(new PeopleBaseItemMap()
using (new TrackedMigrationStep("loading People", _logger))
{
Item = null!,
ItemId = itemId,
People = null!,
PeopleId = personCache.Person.Id,
ListOrder = sortOrder,
SortOrder = sortOrder,
Role = role
});
}
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));
continue;
}
baseItemIds.Clear();
var entity = GetPerson(reader);
if (!peopleCache.TryGetValue(entity.Name, out var personCache))
{
peopleCache[entity.Name] = personCache = (entity, []);
}
foreach (var item in peopleCache)
{
dbContext.Peoples.Add(item.Value.Person);
dbContext.PeopleBaseItemMap.AddRange(item.Value.Items.DistinctBy(e => (e.ItemId, e.PeopleId)));
}
if (reader.TryGetString(2, out var role))
{
}
peopleCache.Clear();
int? sortOrder = reader.IsDBNull(4) ? null : reader.GetInt32(4);
_logger.LogInformation("Try saving {0} People entries.", dbContext.Peoples.Local.Count);
dbContext.SaveChanges();
migrationTotalTime += stopwatch.Elapsed;
_logger.LogInformation("Saving People entries took {0}.", stopwatch.Elapsed);
stopwatch.Restart();
personCache.Items.Add(new PeopleBaseItemMap()
{
Item = null!,
ItemId = itemId,
People = null!,
PeopleId = personCache.Person.Id,
ListOrder = sortOrder,
SortOrder = sortOrder,
Role = role
});
}
_logger.LogInformation("Start moving Chapters.");
const string chapterQuery = """
SELECT ItemId,StartPositionTicks,Name,ImagePath,ImageDateModified,ChapterIndex from Chapters2
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = Chapters2.ItemId)
""";
dbContext.Chapters.ExecuteDelete();
baseItemIds.Clear();
foreach (SqliteDataReader dto in connection.Query(chapterQuery))
{
var chapter = GetChapter(dto);
dbContext.Chapters.Add(chapter);
}
foreach (var item in peopleCache)
{
operation.JellyfinDbContext.Peoples.Add(item.Value.Person);
operation.JellyfinDbContext.PeopleBaseItemMap.AddRange(item.Value.Items.DistinctBy(e => (e.ItemId, e.PeopleId)));
}
_logger.LogInformation("Try saving {0} Chapters entries.", dbContext.Chapters.Local.Count);
dbContext.SaveChanges();
migrationTotalTime += stopwatch.Elapsed;
_logger.LogInformation("Saving Chapters took {0}.", stopwatch.Elapsed);
stopwatch.Restart();
peopleCache.Clear();
}
_logger.LogInformation("Start moving AncestorIds.");
const string ancestorIdsQuery = """
SELECT ItemId, AncestorId, AncestorIdText FROM AncestorIds
WHERE
EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.ItemId)
AND
EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.AncestorId)
""";
dbContext.AncestorIds.ExecuteDelete();
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.Peoples.Local.Count} People entries and {operation.JellyfinDbContext.PeopleBaseItemMap.Local.Count} maps", _logger))
{
operation.JellyfinDbContext.SaveChanges();
}
}
foreach (SqliteDataReader dto in connection.Query(ancestorIdsQuery))
using (var operation = GetPreparedDbContext("moving Chapters"))
{
var ancestorId = GetAncestorId(dto);
dbContext.AncestorIds.Add(ancestorId);
const string chapterQuery =
"""
SELECT ItemId,StartPositionTicks,Name,ImagePath,ImageDateModified,ChapterIndex from Chapters2
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = Chapters2.ItemId)
""";
using (new TrackedMigrationStep("loading Chapters", _logger))
{
foreach (SqliteDataReader dto in connection.Query(chapterQuery))
{
var chapter = GetChapter(dto);
operation.JellyfinDbContext.Chapters.Add(chapter);
}
}
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.Chapters.Local.Count} Chapters entries", _logger))
{
operation.JellyfinDbContext.SaveChanges();
}
}
_logger.LogInformation("Try saving {0} AncestorIds entries.", dbContext.AncestorIds.Local.Count);
using (var operation = GetPreparedDbContext("moving AncestorIds"))
{
const string ancestorIdsQuery =
"""
SELECT ItemId, AncestorId, AncestorIdText FROM AncestorIds
WHERE
EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.ItemId)
AND
EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.AncestorId)
""";
dbContext.SaveChanges();
migrationTotalTime += stopwatch.Elapsed;
_logger.LogInformation("Saving AncestorIds took {0}.", stopwatch.Elapsed);
stopwatch.Restart();
using (new TrackedMigrationStep("loading AncestorIds", _logger))
{
foreach (SqliteDataReader dto in connection.Query(ancestorIdsQuery))
{
var ancestorId = GetAncestorId(dto);
operation.JellyfinDbContext.AncestorIds.Add(ancestorId);
}
}
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.AncestorIds.Local.Count} AncestorId entries", _logger))
{
operation.JellyfinDbContext.SaveChanges();
}
}
connection.Close();
_logger.LogInformation("Migration of the Library.db done.");
_logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old");
_logger.LogInformation("Migrating Library db took {0}.", fullOperationTimer.Elapsed);
SqliteConnection.ClearAllPools();
_logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old");
File.Move(libraryDbPath, libraryDbPath + ".old", true);
_logger.LogInformation("Migrating Library db took {0}.", migrationTotalTime);
_jellyfinDatabaseProvider.RunScheduledOptimisation(CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult();
}
private UserData? GetUserData(ImmutableArray<User> users, SqliteDataReader dto)
private DatabaseMigrationStep GetPreparedDbContext(string operationName)
{
var dbContext = _provider.CreateDbContext();
dbContext.ChangeTracker.AutoDetectChangesEnabled = false;
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
return new DatabaseMigrationStep(dbContext, operationName, _logger);
}
private UserData? GetUserData(ImmutableArray<User> users, SqliteDataReader dto, HashSet<int> userIdBlacklist)
{
var internalUserId = dto.GetInt32(1);
var user = users.FirstOrDefault(e => e.InternalId == internalUserId);
if (user is null)
{
if (userIdBlacklist.Contains(internalUserId))
{
return null;
}
_logger.LogError("Tried to find user with index '{Idx}' but there are only '{MaxIdx}' users.", internalUserId, users.Length);
return null;
}
@ -1214,4 +1269,58 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
return image;
}
private class TrackedMigrationStep : IDisposable
{
private readonly string _operationName;
private readonly ILogger _logger;
private readonly Stopwatch _operationTimer;
private bool _disposed;
public TrackedMigrationStep(string operationName, ILogger logger)
{
_operationName = operationName;
_logger = logger;
_operationTimer = Stopwatch.StartNew();
logger.LogInformation("Start {OperationName}", operationName);
}
public bool Disposed
{
get => _disposed;
set => _disposed = value;
}
public virtual void Dispose()
{
if (Disposed)
{
return;
}
Disposed = true;
_logger.LogInformation("{OperationName} took '{Time}'", _operationName, _operationTimer.Elapsed);
}
}
private sealed class DatabaseMigrationStep : TrackedMigrationStep
{
public DatabaseMigrationStep(JellyfinDbContext jellyfinDbContext, string operationName, ILogger logger) : base(operationName, logger)
{
JellyfinDbContext = jellyfinDbContext;
}
public JellyfinDbContext JellyfinDbContext { get; }
public override void Dispose()
{
if (Disposed)
{
return;
}
JellyfinDbContext.Dispose();
base.Dispose();
}
}
}

Loading…
Cancel
Save