< Summary - Jellyfin

Information
Class: Jellyfin.Server.Migrations.Routines.MigrateLibraryDb
Assembly: jellyfin
File(s): /srv/git/jellyfin/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 612
Coverable lines: 612
Total lines: 1389
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 340
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
Perform()0%1482380%
GetPreparedDbContext(...)100%210%
GetUserData(...)0%156120%
GetAncestorId(...)100%210%
GetChapter(...)0%4260%
GetItemValue(...)100%210%
GetPerson(...)0%620%
GetMediaStream(...)0%5256720%
GetMediaAttachment(...)0%110100%
GetItem(...)0%226501500%
Map(...)0%620%
DeserializeImages(...)0%110100%
ItemImageInfoFromValueString(...)0%1190340%
.ctor(...)100%210%
get_Disposed()100%210%
set_Disposed(...)100%210%
Dispose()0%620%
.ctor(...)100%210%
Dispose()0%620%

File(s)

/srv/git/jellyfin/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs

#LineLine coverage
 1#pragma warning disable RS0030 // Do not use banned APIs
 2
 3using System;
 4using System.Collections.Generic;
 5using System.Collections.Immutable;
 6using System.Data;
 7using System.Diagnostics;
 8using System.Globalization;
 9using System.IO;
 10using System.Linq;
 11using System.Text;
 12using Emby.Server.Implementations.Data;
 13using Jellyfin.Database.Implementations;
 14using Jellyfin.Database.Implementations.Entities;
 15using Jellyfin.Extensions;
 16using Jellyfin.Server.Implementations.Item;
 17using MediaBrowser.Controller;
 18using MediaBrowser.Controller.Entities;
 19using MediaBrowser.Model.Entities;
 20using Microsoft.Data.Sqlite;
 21using Microsoft.EntityFrameworkCore;
 22using Microsoft.Extensions.Logging;
 23using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity;
 24using Chapter = Jellyfin.Database.Implementations.Entities.Chapter;
 25
 26namespace Jellyfin.Server.Migrations.Routines;
 27
 28/// <summary>
 29/// The migration routine for migrating the userdata database to EF Core.
 30/// </summary>
 31[JellyfinMigration("2025-04-20T20:00:00", nameof(MigrateLibraryDb))]
 32internal class MigrateLibraryDb : IDatabaseMigrationRoutine
 33{
 34    private const string DbFilename = "library.db";
 35
 36    private readonly ILogger<MigrateLibraryDb> _logger;
 37    private readonly IServerApplicationPaths _paths;
 38    private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider;
 39    private readonly IDbContextFactory<JellyfinDbContext> _provider;
 40
 41    /// <summary>
 42    /// Initializes a new instance of the <see cref="MigrateLibraryDb"/> class.
 43    /// </summary>
 44    /// <param name="logger">The logger.</param>
 45    /// <param name="provider">The database provider.</param>
 46    /// <param name="paths">The server application paths.</param>
 47    /// <param name="jellyfinDatabaseProvider">The database provider for special access.</param>
 48    /// <param name="serviceProvider">The Service provider.</param>
 49    public MigrateLibraryDb(
 50        ILogger<MigrateLibraryDb> logger,
 51        IDbContextFactory<JellyfinDbContext> provider,
 52        IServerApplicationPaths paths,
 53        IJellyfinDatabaseProvider jellyfinDatabaseProvider,
 54        IServiceProvider serviceProvider)
 55    {
 056        _logger = logger;
 057        _provider = provider;
 058        _paths = paths;
 059        _jellyfinDatabaseProvider = jellyfinDatabaseProvider;
 060    }
 61
 62    /// <inheritdoc/>
 63    public void Perform()
 64    {
 065        _logger.LogInformation("Migrating the userdata from library.db may take a while, do not stop Jellyfin.");
 66
 067        var dataPath = _paths.DataPath;
 068        var libraryDbPath = Path.Combine(dataPath, DbFilename);
 069        if (!File.Exists(libraryDbPath))
 70        {
 071            _logger.LogError("Cannot migrate {LibraryDb} as it does not exist..", libraryDbPath);
 072            return;
 73        }
 74
 075        using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly");
 76
 077        var fullOperationTimer = new Stopwatch();
 078        fullOperationTimer.Start();
 79
 080        using (var operation = GetPreparedDbContext("Cleanup database"))
 81        {
 082            operation.JellyfinDbContext.AttachmentStreamInfos.ExecuteDelete();
 083            operation.JellyfinDbContext.BaseItems.ExecuteDelete();
 084            operation.JellyfinDbContext.ItemValues.ExecuteDelete();
 085            operation.JellyfinDbContext.UserData.ExecuteDelete();
 086            operation.JellyfinDbContext.MediaStreamInfos.ExecuteDelete();
 087            operation.JellyfinDbContext.Peoples.ExecuteDelete();
 088            operation.JellyfinDbContext.PeopleBaseItemMap.ExecuteDelete();
 089            operation.JellyfinDbContext.Chapters.ExecuteDelete();
 090            operation.JellyfinDbContext.AncestorIds.ExecuteDelete();
 091        }
 92
 093        var legacyBaseItemWithUserKeys = new Dictionary<string, BaseItemEntity>();
 094        connection.Open();
 95
 096        var baseItemIds = new HashSet<Guid>();
 097        using (var operation = GetPreparedDbContext("moving TypedBaseItem"))
 98        {
 99            const string typedBaseItemsQuery =
 100            """
 101            SELECT guid, type, data, StartDate, EndDate, ChannelId, IsMovie,
 102            IsSeries, EpisodeTitle, IsRepeat, CommunityRating, CustomRating, IndexNumber, IsLocked, PreferredMetadataLan
 103            PreferredMetadataCountryCode, Width, Height, DateLastRefreshed, Name, Path, PremiereDate, Overview, ParentIn
 104            ProductionYear, OfficialRating, ForcedSortName, RunTimeTicks, Size, DateCreated, DateModified, Genres, Paren
 105            Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, Origina
 106            DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, Se
 107            PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, Product
 108            ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortN
 109            """;
 0110            using (new TrackedMigrationStep("Loading TypedBaseItems", _logger))
 111            {
 0112                foreach (SqliteDataReader dto in connection.Query(typedBaseItemsQuery))
 113                {
 0114                    var baseItem = GetItem(dto);
 0115                    operation.JellyfinDbContext.BaseItems.Add(baseItem.BaseItem);
 0116                    baseItemIds.Add(baseItem.BaseItem.Id);
 0117                    foreach (var dataKey in baseItem.LegacyUserDataKey)
 118                    {
 0119                        legacyBaseItemWithUserKeys[dataKey] = baseItem.BaseItem;
 120                    }
 121                }
 122            }
 123
 0124            using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.BaseItems.Local.Count} BaseItem entrie
 125            {
 0126                operation.JellyfinDbContext.SaveChanges();
 0127            }
 128        }
 129
 0130        using (var operation = GetPreparedDbContext("moving ItemValues"))
 131        {
 132            // do not migrate inherited types as they are now properly mapped in search and lookup.
 133            const string itemValueQuery =
 134            """
 135            SELECT ItemId, Type, Value, CleanValue FROM ItemValues
 136                        WHERE Type <> 6 AND EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = ItemValues.I
 137            """;
 138
 139            // EFCores local lookup sucks. We cannot use context.ItemValues.Local here because its just super slow.
 0140            var localItems = new Dictionary<(int Type, string Value), (Database.Implementations.Entities.ItemValue ItemV
 0141            using (new TrackedMigrationStep("loading ItemValues", _logger))
 142            {
 0143                foreach (SqliteDataReader dto in connection.Query(itemValueQuery))
 144                {
 0145                    var itemId = dto.GetGuid(0);
 0146                    var entity = GetItemValue(dto);
 0147                    var key = ((int)entity.Type, entity.Value);
 0148                    if (!localItems.TryGetValue(key, out var existing))
 149                    {
 0150                        localItems[key] = existing = (entity, []);
 151                    }
 152
 0153                    existing.ItemIds.Add(itemId);
 154                }
 155
 0156                foreach (var item in localItems)
 157                {
 0158                    operation.JellyfinDbContext.ItemValues.Add(item.Value.ItemValue);
 0159                    operation.JellyfinDbContext.ItemValuesMap.AddRange(item.Value.ItemIds.Distinct().Select(f => new Ite
 0160                    {
 0161                        Item = null!,
 0162                        ItemValue = null!,
 0163                        ItemId = f,
 0164                        ItemValueId = item.Value.ItemValue.ItemValueId
 0165                    }));
 166                }
 167            }
 168
 0169            using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.ItemValues.Local.Count} ItemValues ent
 170            {
 0171                operation.JellyfinDbContext.SaveChanges();
 0172            }
 173        }
 174
 0175        using (var operation = GetPreparedDbContext("moving UserData"))
 176        {
 0177            var queryResult = connection.Query(
 0178            """
 0179            SELECT key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStrea
 0180
 0181            WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.UserDataKey = UserDatas.key)
 0182            """);
 183
 0184            using (new TrackedMigrationStep("loading UserData", _logger))
 185            {
 0186                var users = operation.JellyfinDbContext.Users.AsNoTracking().ToImmutableArray();
 0187                var userIdBlacklist = new HashSet<int>();
 188
 0189                foreach (var entity in queryResult)
 190                {
 0191                    var userData = GetUserData(users, entity, userIdBlacklist);
 0192                    if (userData is null)
 193                    {
 0194                        var userDataId = entity.GetString(0);
 0195                        var internalUserId = entity.GetInt32(1);
 196
 0197                        if (!userIdBlacklist.Contains(internalUserId))
 198                        {
 0199                            _logger.LogError("Was not able to migrate user data with key {0} because its id {InternalId}
 0200                            userIdBlacklist.Add(internalUserId);
 201                        }
 202
 0203                        continue;
 204                    }
 205
 0206                    if (!legacyBaseItemWithUserKeys.TryGetValue(userData.CustomDataKey!, out var refItem))
 207                    {
 0208                        _logger.LogError("Was not able to migrate user data with key {0} because it does not reference a
 0209                        continue;
 210                    }
 211
 0212                    userData.ItemId = refItem.Id;
 0213                    operation.JellyfinDbContext.UserData.Add(userData);
 214                }
 215
 0216                users.Clear();
 0217            }
 218
 0219            legacyBaseItemWithUserKeys.Clear();
 220
 0221            using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.UserData.Local.Count} UserData entries
 222            {
 0223                operation.JellyfinDbContext.SaveChanges();
 0224            }
 225        }
 226
 0227        using (var operation = GetPreparedDbContext("moving MediaStreamInfos"))
 228        {
 229            const string mediaStreamQuery =
 230            """
 231            SELECT ItemId, StreamIndex, StreamType, Codec, Language, ChannelLayout, Profile, AspectRatio, Path,
 232            IsInterlaced, BitRate, Channels, SampleRate, IsDefault, IsForced, IsExternal, Height, Width,
 233            AverageFrameRate, RealFrameRate, Level, PixelFormat, BitDepth, IsAnamorphic, RefFrames, CodecTag,
 234            Comment, NalLengthSize, IsAvc, Title, TimeBase, CodecTimeBase, ColorPrimaries, ColorSpace, ColorTransfer,
 235            DvVersionMajor, DvVersionMinor, DvProfile, DvLevel, RpuPresentFlag, ElPresentFlag, BlPresentFlag, DvBlSignal
 236            FROM MediaStreams
 237            WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = MediaStreams.ItemId)
 238            """;
 239
 0240            using (new TrackedMigrationStep("loading MediaStreamInfos", _logger))
 241            {
 0242                foreach (SqliteDataReader dto in connection.Query(mediaStreamQuery))
 243                {
 0244                    operation.JellyfinDbContext.MediaStreamInfos.Add(GetMediaStream(dto));
 245                }
 246            }
 247
 0248            using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.MediaStreamInfos.Local.Count} MediaStr
 249            {
 0250                operation.JellyfinDbContext.SaveChanges();
 0251            }
 252        }
 253
 0254        using (var operation = GetPreparedDbContext("moving AttachmentStreamInfos"))
 255        {
 256            const string mediaAttachmentQuery =
 257            """
 258            SELECT ItemId, AttachmentIndex, Codec, CodecTag, Comment, filename, MIMEType
 259            FROM mediaattachments
 260            WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = mediaattachments.ItemId)
 261            """;
 262
 0263            using (new TrackedMigrationStep("loading AttachmentStreamInfos", _logger))
 264            {
 0265                foreach (SqliteDataReader dto in connection.Query(mediaAttachmentQuery))
 266                {
 0267                    operation.JellyfinDbContext.AttachmentStreamInfos.Add(GetMediaAttachment(dto));
 268                }
 269            }
 270
 0271            using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.AttachmentStreamInfos.Local.Count} Att
 272            {
 0273                operation.JellyfinDbContext.SaveChanges();
 0274            }
 275        }
 276
 0277        using (var operation = GetPreparedDbContext("moving People"))
 278        {
 279            const string personsQuery =
 280            """
 281            SELECT ItemId, Name, Role, PersonType, SortOrder FROM People
 282            WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = People.ItemId)
 283            """;
 284
 0285            var peopleCache = new Dictionary<string, (People Person, List<PeopleBaseItemMap> Items)>();
 286
 0287            using (new TrackedMigrationStep("loading People", _logger))
 288            {
 0289                foreach (SqliteDataReader reader in connection.Query(personsQuery))
 290                {
 0291                    var itemId = reader.GetGuid(0);
 0292                    if (!baseItemIds.Contains(itemId))
 293                    {
 0294                        _logger.LogError("Dont save person {0} because its not in use by any BaseItem", reader.GetString
 0295                        continue;
 296                    }
 297
 0298                    var entity = GetPerson(reader);
 0299                    if (!peopleCache.TryGetValue(entity.Name, out var personCache))
 300                    {
 0301                        peopleCache[entity.Name] = personCache = (entity, []);
 302                    }
 303
 0304                    if (reader.TryGetString(2, out var role))
 305                    {
 306                    }
 307
 0308                    int? sortOrder = reader.IsDBNull(4) ? null : reader.GetInt32(4);
 309
 0310                    personCache.Items.Add(new PeopleBaseItemMap()
 0311                    {
 0312                        Item = null!,
 0313                        ItemId = itemId,
 0314                        People = null!,
 0315                        PeopleId = personCache.Person.Id,
 0316                        ListOrder = sortOrder,
 0317                        SortOrder = sortOrder,
 0318                        Role = role
 0319                    });
 320                }
 321
 0322                baseItemIds.Clear();
 323
 0324                foreach (var item in peopleCache)
 325                {
 0326                    operation.JellyfinDbContext.Peoples.Add(item.Value.Person);
 0327                    operation.JellyfinDbContext.PeopleBaseItemMap.AddRange(item.Value.Items.DistinctBy(e => (e.ItemId, e
 328                }
 329
 0330                peopleCache.Clear();
 0331            }
 332
 0333            using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.Peoples.Local.Count} People entries an
 334            {
 0335                operation.JellyfinDbContext.SaveChanges();
 0336            }
 337        }
 338
 0339        using (var operation = GetPreparedDbContext("moving Chapters"))
 340        {
 341            const string chapterQuery =
 342            """
 343            SELECT ItemId,StartPositionTicks,Name,ImagePath,ImageDateModified,ChapterIndex from Chapters2
 344            WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = Chapters2.ItemId)
 345            """;
 346
 0347            using (new TrackedMigrationStep("loading Chapters", _logger))
 348            {
 0349                foreach (SqliteDataReader dto in connection.Query(chapterQuery))
 350                {
 0351                    var chapter = GetChapter(dto);
 0352                    operation.JellyfinDbContext.Chapters.Add(chapter);
 353                }
 354            }
 355
 0356            using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.Chapters.Local.Count} Chapters entries
 357            {
 0358                operation.JellyfinDbContext.SaveChanges();
 0359            }
 360        }
 361
 0362        using (var operation = GetPreparedDbContext("moving AncestorIds"))
 363        {
 364            const string ancestorIdsQuery =
 365            """
 366            SELECT ItemId, AncestorId, AncestorIdText FROM AncestorIds
 367            WHERE
 368            EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.ItemId)
 369            AND
 370            EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.AncestorId)
 371            """;
 372
 0373            using (new TrackedMigrationStep("loading AncestorIds", _logger))
 374            {
 0375                foreach (SqliteDataReader dto in connection.Query(ancestorIdsQuery))
 376                {
 0377                    var ancestorId = GetAncestorId(dto);
 0378                    operation.JellyfinDbContext.AncestorIds.Add(ancestorId);
 379                }
 380            }
 381
 0382            using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.AncestorIds.Local.Count} AncestorId en
 383            {
 0384                operation.JellyfinDbContext.SaveChanges();
 0385            }
 386        }
 387
 0388        connection.Close();
 389
 0390        _logger.LogInformation("Migration of the Library.db done.");
 0391        _logger.LogInformation("Migrating Library db took {0}.", fullOperationTimer.Elapsed);
 392
 0393        SqliteConnection.ClearAllPools();
 394
 0395        _logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old");
 0396        File.Move(libraryDbPath, libraryDbPath + ".old", true);
 0397    }
 398
 399    private DatabaseMigrationStep GetPreparedDbContext(string operationName)
 400    {
 0401        var dbContext = _provider.CreateDbContext();
 0402        dbContext.ChangeTracker.AutoDetectChangesEnabled = false;
 0403        dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
 0404        return new DatabaseMigrationStep(dbContext, operationName, _logger);
 405    }
 406
 407    private UserData? GetUserData(ImmutableArray<User> users, SqliteDataReader dto, HashSet<int> userIdBlacklist)
 408    {
 0409        var internalUserId = dto.GetInt32(1);
 0410        var user = users.FirstOrDefault(e => e.InternalId == internalUserId);
 411
 0412        if (user is null)
 413        {
 0414            if (userIdBlacklist.Contains(internalUserId))
 415            {
 0416                return null;
 417            }
 418
 0419            _logger.LogError("Tried to find user with index '{Idx}' but there are only '{MaxIdx}' users.", internalUserI
 0420            return null;
 421        }
 422
 0423        var oldKey = dto.GetString(0);
 424
 0425        return new UserData()
 0426        {
 0427            ItemId = Guid.NewGuid(),
 0428            CustomDataKey = oldKey,
 0429            UserId = user.Id,
 0430            Rating = dto.IsDBNull(2) ? null : dto.GetDouble(2),
 0431            Played = dto.GetBoolean(3),
 0432            PlayCount = dto.GetInt32(4),
 0433            IsFavorite = dto.GetBoolean(5),
 0434            PlaybackPositionTicks = dto.GetInt64(6),
 0435            LastPlayedDate = dto.IsDBNull(7) ? null : dto.GetDateTime(7),
 0436            AudioStreamIndex = dto.IsDBNull(8) ? null : dto.GetInt32(8),
 0437            SubtitleStreamIndex = dto.IsDBNull(9) ? null : dto.GetInt32(9),
 0438            Likes = null,
 0439            User = null!,
 0440            Item = null!
 0441        };
 442    }
 443
 444    private AncestorId GetAncestorId(SqliteDataReader reader)
 445    {
 0446        return new AncestorId()
 0447        {
 0448            ItemId = reader.GetGuid(0),
 0449            ParentItemId = reader.GetGuid(1),
 0450            Item = null!,
 0451            ParentItem = null!
 0452        };
 453    }
 454
 455    /// <summary>
 456    /// Gets the chapter.
 457    /// </summary>
 458    /// <param name="reader">The reader.</param>
 459    /// <returns>ChapterInfo.</returns>
 460    private Chapter GetChapter(SqliteDataReader reader)
 461    {
 0462        var chapter = new Chapter
 0463        {
 0464            StartPositionTicks = reader.GetInt64(1),
 0465            ChapterIndex = reader.GetInt32(5),
 0466            Item = null!,
 0467            ItemId = reader.GetGuid(0),
 0468        };
 469
 0470        if (reader.TryGetString(2, out var chapterName))
 471        {
 0472            chapter.Name = chapterName;
 473        }
 474
 0475        if (reader.TryGetString(3, out var imagePath))
 476        {
 0477            chapter.ImagePath = imagePath;
 478        }
 479
 0480        if (reader.TryReadDateTime(4, out var imageDateModified))
 481        {
 0482            chapter.ImageDateModified = imageDateModified;
 483        }
 484
 0485        return chapter;
 486    }
 487
 488    private ItemValue GetItemValue(SqliteDataReader reader)
 489    {
 0490        return new ItemValue
 0491        {
 0492            ItemValueId = Guid.NewGuid(),
 0493            Type = (ItemValueType)reader.GetInt32(1),
 0494            Value = reader.GetString(2),
 0495            CleanValue = reader.GetString(3),
 0496        };
 497    }
 498
 499    private People GetPerson(SqliteDataReader reader)
 500    {
 0501        var item = new People
 0502        {
 0503            Id = Guid.NewGuid(),
 0504            Name = reader.GetString(1),
 0505        };
 506
 0507        if (reader.TryGetString(3, out var type))
 508        {
 0509            item.PersonType = type;
 510        }
 511
 0512        return item;
 513    }
 514
 515    /// <summary>
 516    /// Gets the media stream.
 517    /// </summary>
 518    /// <param name="reader">The reader.</param>
 519    /// <returns>MediaStream.</returns>
 520    private MediaStreamInfo GetMediaStream(SqliteDataReader reader)
 521    {
 0522        var item = new MediaStreamInfo
 0523        {
 0524            StreamIndex = reader.GetInt32(1),
 0525            StreamType = Enum.Parse<MediaStreamTypeEntity>(reader.GetString(2)),
 0526            Item = null!,
 0527            ItemId = reader.GetGuid(0),
 0528            AspectRatio = null!,
 0529            ChannelLayout = null!,
 0530            Codec = null!,
 0531            IsInterlaced = false,
 0532            Language = null!,
 0533            Path = null!,
 0534            Profile = null!,
 0535        };
 536
 0537        if (reader.TryGetString(3, out var codec))
 538        {
 0539            item.Codec = codec;
 540        }
 541
 0542        if (reader.TryGetString(4, out var language))
 543        {
 0544            item.Language = language;
 545        }
 546
 0547        if (reader.TryGetString(5, out var channelLayout))
 548        {
 0549            item.ChannelLayout = channelLayout;
 550        }
 551
 0552        if (reader.TryGetString(6, out var profile))
 553        {
 0554            item.Profile = profile;
 555        }
 556
 0557        if (reader.TryGetString(7, out var aspectRatio))
 558        {
 0559            item.AspectRatio = aspectRatio;
 560        }
 561
 0562        if (reader.TryGetString(8, out var path))
 563        {
 0564            item.Path = path;
 565        }
 566
 0567        item.IsInterlaced = reader.GetBoolean(9);
 568
 0569        if (reader.TryGetInt32(10, out var bitrate))
 570        {
 0571            item.BitRate = bitrate;
 572        }
 573
 0574        if (reader.TryGetInt32(11, out var channels))
 575        {
 0576            item.Channels = channels;
 577        }
 578
 0579        if (reader.TryGetInt32(12, out var sampleRate))
 580        {
 0581            item.SampleRate = sampleRate;
 582        }
 583
 0584        item.IsDefault = reader.GetBoolean(13);
 0585        item.IsForced = reader.GetBoolean(14);
 0586        item.IsExternal = reader.GetBoolean(15);
 587
 0588        if (reader.TryGetInt32(16, out var width))
 589        {
 0590            item.Width = width;
 591        }
 592
 0593        if (reader.TryGetInt32(17, out var height))
 594        {
 0595            item.Height = height;
 596        }
 597
 0598        if (reader.TryGetSingle(18, out var averageFrameRate))
 599        {
 0600            item.AverageFrameRate = averageFrameRate;
 601        }
 602
 0603        if (reader.TryGetSingle(19, out var realFrameRate))
 604        {
 0605            item.RealFrameRate = realFrameRate;
 606        }
 607
 0608        if (reader.TryGetSingle(20, out var level))
 609        {
 0610            item.Level = level;
 611        }
 612
 0613        if (reader.TryGetString(21, out var pixelFormat))
 614        {
 0615            item.PixelFormat = pixelFormat;
 616        }
 617
 0618        if (reader.TryGetInt32(22, out var bitDepth))
 619        {
 0620            item.BitDepth = bitDepth;
 621        }
 622
 0623        if (reader.TryGetBoolean(23, out var isAnamorphic))
 624        {
 0625            item.IsAnamorphic = isAnamorphic;
 626        }
 627
 0628        if (reader.TryGetInt32(24, out var refFrames))
 629        {
 0630            item.RefFrames = refFrames;
 631        }
 632
 0633        if (reader.TryGetString(25, out var codecTag))
 634        {
 0635            item.CodecTag = codecTag;
 636        }
 637
 0638        if (reader.TryGetString(26, out var comment))
 639        {
 0640            item.Comment = comment;
 641        }
 642
 0643        if (reader.TryGetString(27, out var nalLengthSize))
 644        {
 0645            item.NalLengthSize = nalLengthSize;
 646        }
 647
 0648        if (reader.TryGetBoolean(28, out var isAVC))
 649        {
 0650            item.IsAvc = isAVC;
 651        }
 652
 0653        if (reader.TryGetString(29, out var title))
 654        {
 0655            item.Title = title;
 656        }
 657
 0658        if (reader.TryGetString(30, out var timeBase))
 659        {
 0660            item.TimeBase = timeBase;
 661        }
 662
 0663        if (reader.TryGetString(31, out var codecTimeBase))
 664        {
 0665            item.CodecTimeBase = codecTimeBase;
 666        }
 667
 0668        if (reader.TryGetString(32, out var colorPrimaries))
 669        {
 0670            item.ColorPrimaries = colorPrimaries;
 671        }
 672
 0673        if (reader.TryGetString(33, out var colorSpace))
 674        {
 0675            item.ColorSpace = colorSpace;
 676        }
 677
 0678        if (reader.TryGetString(34, out var colorTransfer))
 679        {
 0680            item.ColorTransfer = colorTransfer;
 681        }
 682
 0683        if (reader.TryGetInt32(35, out var dvVersionMajor))
 684        {
 0685            item.DvVersionMajor = dvVersionMajor;
 686        }
 687
 0688        if (reader.TryGetInt32(36, out var dvVersionMinor))
 689        {
 0690            item.DvVersionMinor = dvVersionMinor;
 691        }
 692
 0693        if (reader.TryGetInt32(37, out var dvProfile))
 694        {
 0695            item.DvProfile = dvProfile;
 696        }
 697
 0698        if (reader.TryGetInt32(38, out var dvLevel))
 699        {
 0700            item.DvLevel = dvLevel;
 701        }
 702
 0703        if (reader.TryGetInt32(39, out var rpuPresentFlag))
 704        {
 0705            item.RpuPresentFlag = rpuPresentFlag;
 706        }
 707
 0708        if (reader.TryGetInt32(40, out var elPresentFlag))
 709        {
 0710            item.ElPresentFlag = elPresentFlag;
 711        }
 712
 0713        if (reader.TryGetInt32(41, out var blPresentFlag))
 714        {
 0715            item.BlPresentFlag = blPresentFlag;
 716        }
 717
 0718        if (reader.TryGetInt32(42, out var dvBlSignalCompatibilityId))
 719        {
 0720            item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId;
 721        }
 722
 0723        item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result;
 724
 725        // if (reader.TryGetInt32(44, out var rotation))
 726        // {
 727        //     item.Rotation = rotation;
 728        // }
 729
 0730        return item;
 731    }
 732
 733    /// <summary>
 734    /// Gets the attachment.
 735    /// </summary>
 736    /// <param name="reader">The reader.</param>
 737    /// <returns>MediaAttachment.</returns>
 738    private AttachmentStreamInfo GetMediaAttachment(SqliteDataReader reader)
 739    {
 0740        var item = new AttachmentStreamInfo
 0741        {
 0742            Index = reader.GetInt32(1),
 0743            Item = null!,
 0744            ItemId = reader.GetGuid(0),
 0745        };
 746
 0747        if (reader.TryGetString(2, out var codec))
 748        {
 0749            item.Codec = codec;
 750        }
 751
 0752        if (reader.TryGetString(3, out var codecTag))
 753        {
 0754            item.CodecTag = codecTag;
 755        }
 756
 0757        if (reader.TryGetString(4, out var comment))
 758        {
 0759            item.Comment = comment;
 760        }
 761
 0762        if (reader.TryGetString(5, out var fileName))
 763        {
 0764            item.Filename = fileName;
 765        }
 766
 0767        if (reader.TryGetString(6, out var mimeType))
 768        {
 0769            item.MimeType = mimeType;
 770        }
 771
 0772        return item;
 773    }
 774
 775    private (BaseItemEntity BaseItem, string[] LegacyUserDataKey) GetItem(SqliteDataReader reader)
 776    {
 0777        var entity = new BaseItemEntity()
 0778        {
 0779            Id = reader.GetGuid(0),
 0780            Type = reader.GetString(1),
 0781        };
 782
 0783        var index = 2;
 784
 0785        if (reader.TryGetString(index++, out var data))
 786        {
 0787            entity.Data = data;
 788        }
 789
 0790        if (reader.TryReadDateTime(index++, out var startDate))
 791        {
 0792            entity.StartDate = startDate;
 793        }
 794
 0795        if (reader.TryReadDateTime(index++, out var endDate))
 796        {
 0797            entity.EndDate = endDate;
 798        }
 799
 0800        if (reader.TryGetGuid(index++, out var guid))
 801        {
 0802            entity.ChannelId = guid;
 803        }
 804
 0805        if (reader.TryGetBoolean(index++, out var isMovie))
 806        {
 0807            entity.IsMovie = isMovie;
 808        }
 809
 0810        if (reader.TryGetBoolean(index++, out var isSeries))
 811        {
 0812            entity.IsSeries = isSeries;
 813        }
 814
 0815        if (reader.TryGetString(index++, out var episodeTitle))
 816        {
 0817            entity.EpisodeTitle = episodeTitle;
 818        }
 819
 0820        if (reader.TryGetBoolean(index++, out var isRepeat))
 821        {
 0822            entity.IsRepeat = isRepeat;
 823        }
 824
 0825        if (reader.TryGetSingle(index++, out var communityRating))
 826        {
 0827            entity.CommunityRating = communityRating;
 828        }
 829
 0830        if (reader.TryGetString(index++, out var customRating))
 831        {
 0832            entity.CustomRating = customRating;
 833        }
 834
 0835        if (reader.TryGetInt32(index++, out var indexNumber))
 836        {
 0837            entity.IndexNumber = indexNumber;
 838        }
 839
 0840        if (reader.TryGetBoolean(index++, out var isLocked))
 841        {
 0842            entity.IsLocked = isLocked;
 843        }
 844
 0845        if (reader.TryGetString(index++, out var preferredMetadataLanguage))
 846        {
 0847            entity.PreferredMetadataLanguage = preferredMetadataLanguage;
 848        }
 849
 0850        if (reader.TryGetString(index++, out var preferredMetadataCountryCode))
 851        {
 0852            entity.PreferredMetadataCountryCode = preferredMetadataCountryCode;
 853        }
 854
 0855        if (reader.TryGetInt32(index++, out var width))
 856        {
 0857            entity.Width = width;
 858        }
 859
 0860        if (reader.TryGetInt32(index++, out var height))
 861        {
 0862            entity.Height = height;
 863        }
 864
 0865        if (reader.TryReadDateTime(index++, out var dateLastRefreshed))
 866        {
 0867            entity.DateLastRefreshed = dateLastRefreshed;
 868        }
 869
 0870        if (reader.TryGetString(index++, out var name))
 871        {
 0872            entity.Name = name;
 873        }
 874
 0875        if (reader.TryGetString(index++, out var restorePath))
 876        {
 0877            entity.Path = restorePath;
 878        }
 879
 0880        if (reader.TryReadDateTime(index++, out var premiereDate))
 881        {
 0882            entity.PremiereDate = premiereDate;
 883        }
 884
 0885        if (reader.TryGetString(index++, out var overview))
 886        {
 0887            entity.Overview = overview;
 888        }
 889
 0890        if (reader.TryGetInt32(index++, out var parentIndexNumber))
 891        {
 0892            entity.ParentIndexNumber = parentIndexNumber;
 893        }
 894
 0895        if (reader.TryGetInt32(index++, out var productionYear))
 896        {
 0897            entity.ProductionYear = productionYear;
 898        }
 899
 0900        if (reader.TryGetString(index++, out var officialRating))
 901        {
 0902            entity.OfficialRating = officialRating;
 903        }
 904
 0905        if (reader.TryGetString(index++, out var forcedSortName))
 906        {
 0907            entity.ForcedSortName = forcedSortName;
 908        }
 909
 0910        if (reader.TryGetInt64(index++, out var runTimeTicks))
 911        {
 0912            entity.RunTimeTicks = runTimeTicks;
 913        }
 914
 0915        if (reader.TryGetInt64(index++, out var size))
 916        {
 0917            entity.Size = size;
 918        }
 919
 0920        if (reader.TryReadDateTime(index++, out var dateCreated))
 921        {
 0922            entity.DateCreated = dateCreated;
 923        }
 924
 0925        if (reader.TryReadDateTime(index++, out var dateModified))
 926        {
 0927            entity.DateModified = dateModified;
 928        }
 929
 0930        if (reader.TryGetString(index++, out var genres))
 931        {
 0932            entity.Genres = genres;
 933        }
 934
 0935        if (reader.TryGetGuid(index++, out var parentId))
 936        {
 0937            entity.ParentId = parentId;
 938        }
 939
 0940        if (reader.TryGetGuid(index++, out var topParentId))
 941        {
 0942            entity.TopParentId = topParentId;
 943        }
 944
 0945        if (reader.TryGetString(index++, out var audioString) && Enum.TryParse<ProgramAudioEntity>(audioString, out var 
 946        {
 0947            entity.Audio = audioType;
 948        }
 949
 0950        if (reader.TryGetString(index++, out var serviceName))
 951        {
 0952            entity.ExternalServiceId = serviceName;
 953        }
 954
 0955        if (reader.TryGetBoolean(index++, out var isInMixedFolder))
 956        {
 0957            entity.IsInMixedFolder = isInMixedFolder;
 958        }
 959
 0960        if (reader.TryReadDateTime(index++, out var dateLastSaved))
 961        {
 0962            entity.DateLastSaved = dateLastSaved;
 963        }
 964
 0965        if (reader.TryGetString(index++, out var lockedFields))
 966        {
 0967            entity.LockedFields = lockedFields.Split('|').Select(Enum.Parse<MetadataField>)
 0968                .Select(e => new BaseItemMetadataField()
 0969                {
 0970                    Id = (int)e,
 0971                    Item = entity,
 0972                    ItemId = entity.Id
 0973                })
 0974                .ToArray();
 975        }
 976
 0977        if (reader.TryGetString(index++, out var studios))
 978        {
 0979            entity.Studios = studios;
 980        }
 981
 0982        if (reader.TryGetString(index++, out var tags))
 983        {
 0984            entity.Tags = tags;
 985        }
 986
 0987        if (reader.TryGetString(index++, out var trailerTypes))
 988        {
 0989            entity.TrailerTypes = trailerTypes.Split('|').Select(Enum.Parse<TrailerType>)
 0990                .Select(e => new BaseItemTrailerType()
 0991                {
 0992                    Id = (int)e,
 0993                    Item = entity,
 0994                    ItemId = entity.Id
 0995                })
 0996                .ToArray();
 997        }
 998
 0999        if (reader.TryGetString(index++, out var originalTitle))
 1000        {
 01001            entity.OriginalTitle = originalTitle;
 1002        }
 1003
 01004        if (reader.TryGetString(index++, out var primaryVersionId))
 1005        {
 01006            entity.PrimaryVersionId = primaryVersionId;
 1007        }
 1008
 01009        if (reader.TryReadDateTime(index++, out var dateLastMediaAdded))
 1010        {
 01011            entity.DateLastMediaAdded = dateLastMediaAdded;
 1012        }
 1013
 01014        if (reader.TryGetString(index++, out var album))
 1015        {
 01016            entity.Album = album;
 1017        }
 1018
 01019        if (reader.TryGetSingle(index++, out var lUFS))
 1020        {
 01021            entity.LUFS = lUFS;
 1022        }
 1023
 01024        if (reader.TryGetSingle(index++, out var normalizationGain))
 1025        {
 01026            entity.NormalizationGain = normalizationGain;
 1027        }
 1028
 01029        if (reader.TryGetSingle(index++, out var criticRating))
 1030        {
 01031            entity.CriticRating = criticRating;
 1032        }
 1033
 01034        if (reader.TryGetBoolean(index++, out var isVirtualItem))
 1035        {
 01036            entity.IsVirtualItem = isVirtualItem;
 1037        }
 1038
 01039        if (reader.TryGetString(index++, out var seriesName))
 1040        {
 01041            entity.SeriesName = seriesName;
 1042        }
 1043
 01044        var userDataKeys = new List<string>();
 01045        if (reader.TryGetString(index++, out var directUserDataKey))
 1046        {
 01047            userDataKeys.Add(directUserDataKey);
 1048        }
 1049
 01050        if (reader.TryGetString(index++, out var seasonName))
 1051        {
 01052            entity.SeasonName = seasonName;
 1053        }
 1054
 01055        if (reader.TryGetGuid(index++, out var seasonId))
 1056        {
 01057            entity.SeasonId = seasonId;
 1058        }
 1059
 01060        if (reader.TryGetGuid(index++, out var seriesId))
 1061        {
 01062            entity.SeriesId = seriesId;
 1063        }
 1064
 01065        if (reader.TryGetString(index++, out var presentationUniqueKey))
 1066        {
 01067            entity.PresentationUniqueKey = presentationUniqueKey;
 1068        }
 1069
 01070        if (reader.TryGetInt32(index++, out var parentalRating))
 1071        {
 01072            entity.InheritedParentalRatingValue = parentalRating;
 1073        }
 1074
 01075        if (reader.TryGetString(index++, out var externalSeriesId))
 1076        {
 01077            entity.ExternalSeriesId = externalSeriesId;
 1078        }
 1079
 01080        if (reader.TryGetString(index++, out var tagLine))
 1081        {
 01082            entity.Tagline = tagLine;
 1083        }
 1084
 01085        if (reader.TryGetString(index++, out var providerIds))
 1086        {
 01087            entity.Provider = providerIds.Split('|').Select(e => e.Split("="))
 01088            .Select(e => new BaseItemProvider()
 01089            {
 01090                Item = null!,
 01091                ProviderId = e[0],
 01092                ProviderValue = e[1]
 01093            }).ToArray();
 1094        }
 1095
 01096        if (reader.TryGetString(index++, out var imageInfos))
 1097        {
 01098            entity.Images = DeserializeImages(imageInfos).Select(f => Map(entity.Id, f)).ToArray();
 1099        }
 1100
 01101        if (reader.TryGetString(index++, out var productionLocations))
 1102        {
 01103            entity.ProductionLocations = productionLocations;
 1104        }
 1105
 01106        if (reader.TryGetString(index++, out var extraIds))
 1107        {
 01108            entity.ExtraIds = extraIds;
 1109        }
 1110
 01111        if (reader.TryGetInt32(index++, out var totalBitrate))
 1112        {
 01113            entity.TotalBitrate = totalBitrate;
 1114        }
 1115
 01116        if (reader.TryGetString(index++, out var extraTypeString) && Enum.TryParse<BaseItemExtraType>(extraTypeString, o
 1117        {
 01118            entity.ExtraType = extraType;
 1119        }
 1120
 01121        if (reader.TryGetString(index++, out var artists))
 1122        {
 01123            entity.Artists = artists;
 1124        }
 1125
 01126        if (reader.TryGetString(index++, out var albumArtists))
 1127        {
 01128            entity.AlbumArtists = albumArtists;
 1129        }
 1130
 01131        if (reader.TryGetString(index++, out var externalId))
 1132        {
 01133            entity.ExternalId = externalId;
 1134        }
 1135
 01136        if (reader.TryGetString(index++, out var seriesPresentationUniqueKey))
 1137        {
 01138            entity.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
 1139        }
 1140
 01141        if (reader.TryGetString(index++, out var showId))
 1142        {
 01143            entity.ShowId = showId;
 1144        }
 1145
 01146        if (reader.TryGetString(index++, out var ownerId))
 1147        {
 01148            entity.OwnerId = ownerId;
 1149        }
 1150
 01151        if (reader.TryGetString(index++, out var mediaType))
 1152        {
 01153            entity.MediaType = mediaType;
 1154        }
 1155
 01156        if (reader.TryGetString(index++, out var sortName))
 1157        {
 01158            entity.SortName = sortName;
 1159        }
 1160
 01161        if (reader.TryGetString(index++, out var cleanName))
 1162        {
 01163            entity.CleanName = cleanName;
 1164        }
 1165
 01166        if (reader.TryGetString(index++, out var unratedType))
 1167        {
 01168            entity.UnratedType = unratedType;
 1169        }
 1170
 01171        var baseItem = BaseItemRepository.DeserialiseBaseItem(entity, _logger, null, false);
 01172        var dataKeys = baseItem.GetUserDataKeys();
 01173        userDataKeys.AddRange(dataKeys);
 1174
 01175        return (entity, userDataKeys.ToArray());
 1176    }
 1177
 1178    private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
 1179    {
 01180        return new BaseItemImageInfo()
 01181        {
 01182            ItemId = baseItemId,
 01183            Id = Guid.NewGuid(),
 01184            Path = e.Path,
 01185            Blurhash = e.BlurHash != null ? Encoding.UTF8.GetBytes(e.BlurHash) : null,
 01186            DateModified = e.DateModified,
 01187            Height = e.Height,
 01188            Width = e.Width,
 01189            ImageType = (ImageInfoImageType)e.Type,
 01190            Item = null!
 01191        };
 1192    }
 1193
 1194    internal ItemImageInfo[] DeserializeImages(string value)
 1195    {
 01196        if (string.IsNullOrWhiteSpace(value))
 1197        {
 01198            return Array.Empty<ItemImageInfo>();
 1199        }
 1200
 1201        // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the data
 01202        var valueSpan = value.AsSpan();
 01203        var count = valueSpan.Count('|') + 1;
 1204
 01205        var position = 0;
 01206        var result = new ItemImageInfo[count];
 01207        foreach (var part in valueSpan.Split('|'))
 1208        {
 01209            var image = ItemImageInfoFromValueString(part);
 1210
 01211            if (image is not null)
 1212            {
 01213                result[position++] = image;
 1214            }
 1215        }
 1216
 01217        if (position == count)
 1218        {
 01219            return result;
 1220        }
 1221
 01222        if (position == 0)
 1223        {
 01224            return Array.Empty<ItemImageInfo>();
 1225        }
 1226
 1227        // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array.
 01228        return result[..position];
 1229    }
 1230
 1231    internal ItemImageInfo? ItemImageInfoFromValueString(ReadOnlySpan<char> value)
 1232    {
 1233        const char Delimiter = '*';
 1234
 01235        var nextSegment = value.IndexOf(Delimiter);
 01236        if (nextSegment == -1)
 1237        {
 01238            return null;
 1239        }
 1240
 01241        ReadOnlySpan<char> path = value[..nextSegment];
 01242        value = value[(nextSegment + 1)..];
 01243        nextSegment = value.IndexOf(Delimiter);
 01244        if (nextSegment == -1)
 1245        {
 01246            return null;
 1247        }
 1248
 01249        ReadOnlySpan<char> dateModified = value[..nextSegment];
 01250        value = value[(nextSegment + 1)..];
 01251        nextSegment = value.IndexOf(Delimiter);
 01252        if (nextSegment == -1)
 1253        {
 01254            nextSegment = value.Length;
 1255        }
 1256
 01257        ReadOnlySpan<char> imageType = value[..nextSegment];
 1258
 01259        var image = new ItemImageInfo
 01260        {
 01261            Path = path.ToString()
 01262        };
 1263
 01264        if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks)
 01265            && ticks >= DateTime.MinValue.Ticks
 01266            && ticks <= DateTime.MaxValue.Ticks)
 1267        {
 01268            image.DateModified = new DateTime(ticks, DateTimeKind.Utc);
 1269        }
 1270        else
 1271        {
 01272            return null;
 1273        }
 1274
 01275        if (Enum.TryParse(imageType, true, out ImageType type))
 1276        {
 01277            image.Type = type;
 1278        }
 1279        else
 1280        {
 01281            return null;
 1282        }
 1283
 1284        // Optional parameters: width*height*blurhash
 01285        if (nextSegment + 1 < value.Length - 1)
 1286        {
 01287            value = value[(nextSegment + 1)..];
 01288            nextSegment = value.IndexOf(Delimiter);
 01289            if (nextSegment == -1 || nextSegment == value.Length)
 1290            {
 01291                return image;
 1292            }
 1293
 01294            ReadOnlySpan<char> widthSpan = value[..nextSegment];
 1295
 01296            value = value[(nextSegment + 1)..];
 01297            nextSegment = value.IndexOf(Delimiter);
 01298            if (nextSegment == -1)
 1299            {
 01300                nextSegment = value.Length;
 1301            }
 1302
 01303            ReadOnlySpan<char> heightSpan = value[..nextSegment];
 1304
 01305            if (int.TryParse(widthSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var width)
 01306                && int.TryParse(heightSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var height))
 1307            {
 01308                image.Width = width;
 01309                image.Height = height;
 1310            }
 1311
 01312            if (nextSegment < value.Length - 1)
 1313            {
 01314                value = value[(nextSegment + 1)..];
 01315                var length = value.Length;
 1316
 01317                Span<char> blurHashSpan = stackalloc char[length];
 01318                for (int i = 0; i < length; i++)
 1319                {
 01320                    var c = value[i];
 01321                    blurHashSpan[i] = c switch
 01322                    {
 01323                        '/' => Delimiter,
 01324                        '\\' => '|',
 01325                        _ => c
 01326                    };
 1327                }
 1328
 01329                image.BlurHash = new string(blurHashSpan);
 1330            }
 1331        }
 1332
 01333        return image;
 1334    }
 1335
 1336    private class TrackedMigrationStep : IDisposable
 1337    {
 1338        private readonly string _operationName;
 1339        private readonly ILogger _logger;
 1340        private readonly Stopwatch _operationTimer;
 1341        private bool _disposed;
 1342
 1343        public TrackedMigrationStep(string operationName, ILogger logger)
 1344        {
 01345            _operationName = operationName;
 01346            _logger = logger;
 01347            _operationTimer = Stopwatch.StartNew();
 01348            logger.LogInformation("Start {OperationName}", operationName);
 01349        }
 1350
 1351        public bool Disposed
 1352        {
 01353            get => _disposed;
 01354            set => _disposed = value;
 1355        }
 1356
 1357        public virtual void Dispose()
 1358        {
 01359            if (Disposed)
 1360            {
 01361                return;
 1362            }
 1363
 01364            Disposed = true;
 01365            _logger.LogInformation("{OperationName} took '{Time}'", _operationName, _operationTimer.Elapsed);
 01366        }
 1367    }
 1368
 1369    private sealed class DatabaseMigrationStep : TrackedMigrationStep
 1370    {
 01371        public DatabaseMigrationStep(JellyfinDbContext jellyfinDbContext, string operationName, ILogger logger) : base(o
 1372        {
 1373            JellyfinDbContext = jellyfinDbContext;
 01374        }
 1375
 1376        public JellyfinDbContext JellyfinDbContext { get; }
 1377
 1378        public override void Dispose()
 1379        {
 01380            if (Disposed)
 1381            {
 01382                return;
 1383            }
 1384
 01385            JellyfinDbContext.Dispose();
 01386            base.Dispose();
 01387        }
 1388    }
 1389}