< 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: 613
Coverable lines: 613
Total lines: 1392
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 338
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

0255075100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
get_Id()100%210%
get_Name()100%210%
get_PerformOnNewInstall()100%210%
Perform()0%1332360%
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 System.Threading;
 13using Emby.Server.Implementations.Data;
 14using Jellyfin.Database.Implementations;
 15using Jellyfin.Database.Implementations.Entities;
 16using Jellyfin.Extensions;
 17using Jellyfin.Server.Implementations.Item;
 18using MediaBrowser.Controller;
 19using MediaBrowser.Controller.Entities;
 20using MediaBrowser.Model.Entities;
 21using Microsoft.Data.Sqlite;
 22using Microsoft.EntityFrameworkCore;
 23using Microsoft.Extensions.Logging;
 24using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity;
 25using Chapter = Jellyfin.Database.Implementations.Entities.Chapter;
 26
 27namespace Jellyfin.Server.Migrations.Routines;
 28
 29/// <summary>
 30/// The migration routine for migrating the userdata database to EF Core.
 31/// </summary>
 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    public MigrateLibraryDb(
 49        ILogger<MigrateLibraryDb> logger,
 50        IDbContextFactory<JellyfinDbContext> provider,
 51        IServerApplicationPaths paths,
 52        IJellyfinDatabaseProvider jellyfinDatabaseProvider)
 53    {
 054        _logger = logger;
 055        _provider = provider;
 056        _paths = paths;
 057        _jellyfinDatabaseProvider = jellyfinDatabaseProvider;
 058    }
 59
 60    /// <inheritdoc/>
 061    public Guid Id => Guid.Parse("36445464-849f-429f-9ad0-bb130efa0664");
 62
 63    /// <inheritdoc/>
 064    public string Name => "MigrateLibraryDbData";
 65
 66    /// <inheritdoc/>
 067    public bool PerformOnNewInstall => false; // TODO Change back after testing
 68
 69    /// <inheritdoc/>
 70    public void Perform()
 71    {
 072        _logger.LogInformation("Migrating the userdata from library.db may take a while, do not stop Jellyfin.");
 73
 074        var dataPath = _paths.DataPath;
 075        var libraryDbPath = Path.Combine(dataPath, DbFilename);
 076        using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly");
 77
 078        var fullOperationTimer = new Stopwatch();
 079        fullOperationTimer.Start();
 80
 081        using (var operation = GetPreparedDbContext("Cleanup database"))
 82        {
 083            operation.JellyfinDbContext.AttachmentStreamInfos.ExecuteDelete();
 084            operation.JellyfinDbContext.BaseItems.ExecuteDelete();
 085            operation.JellyfinDbContext.ItemValues.ExecuteDelete();
 086            operation.JellyfinDbContext.UserData.ExecuteDelete();
 087            operation.JellyfinDbContext.MediaStreamInfos.ExecuteDelete();
 088            operation.JellyfinDbContext.Peoples.ExecuteDelete();
 089            operation.JellyfinDbContext.PeopleBaseItemMap.ExecuteDelete();
 090            operation.JellyfinDbContext.Chapters.ExecuteDelete();
 091            operation.JellyfinDbContext.AncestorIds.ExecuteDelete();
 092        }
 93
 094        var legacyBaseItemWithUserKeys = new Dictionary<string, BaseItemEntity>();
 095        connection.Open();
 96
 097        var baseItemIds = new HashSet<Guid>();
 098        using (var operation = GetPreparedDbContext("moving TypedBaseItem"))
 99        {
 100            const string typedBaseItemsQuery =
 101            """
 102            SELECT guid, type, data, StartDate, EndDate, ChannelId, IsMovie,
 103            IsSeries, EpisodeTitle, IsRepeat, CommunityRating, CustomRating, IndexNumber, IsLocked, PreferredMetadataLan
 104            PreferredMetadataCountryCode, Width, Height, DateLastRefreshed, Name, Path, PremiereDate, Overview, ParentIn
 105            ProductionYear, OfficialRating, ForcedSortName, RunTimeTicks, Size, DateCreated, DateModified, Genres, Paren
 106            Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, Origina
 107            DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, Se
 108            PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, Product
 109            ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortN
 110            """;
 0111            using (new TrackedMigrationStep("Loading TypedBaseItems", _logger))
 112            {
 0113                foreach (SqliteDataReader dto in connection.Query(typedBaseItemsQuery))
 114                {
 0115                    var baseItem = GetItem(dto);
 0116                    operation.JellyfinDbContext.BaseItems.Add(baseItem.BaseItem);
 0117                    baseItemIds.Add(baseItem.BaseItem.Id);
 0118                    foreach (var dataKey in baseItem.LegacyUserDataKey)
 119                    {
 0120                        legacyBaseItemWithUserKeys[dataKey] = baseItem.BaseItem;
 121                    }
 122                }
 123            }
 124
 0125            using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.BaseItems.Local.Count} BaseItem entrie
 126            {
 0127                operation.JellyfinDbContext.SaveChanges();
 0128            }
 129        }
 130
 0131        using (var operation = GetPreparedDbContext("moving ItemValues"))
 132        {
 133            // do not migrate inherited types as they are now properly mapped in search and lookup.
 134            const string itemValueQuery =
 135            """
 136            SELECT ItemId, Type, Value, CleanValue FROM ItemValues
 137                        WHERE Type <> 6 AND EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = ItemValues.I
 138            """;
 139
 140            // EFCores local lookup sucks. We cannot use context.ItemValues.Local here because its just super slow.
 0141            var localItems = new Dictionary<(int Type, string Value), (Database.Implementations.Entities.ItemValue ItemV
 0142            using (new TrackedMigrationStep("loading ItemValues", _logger))
 143            {
 0144                foreach (SqliteDataReader dto in connection.Query(itemValueQuery))
 145                {
 0146                    var itemId = dto.GetGuid(0);
 0147                    var entity = GetItemValue(dto);
 0148                    var key = ((int)entity.Type, entity.Value);
 0149                    if (!localItems.TryGetValue(key, out var existing))
 150                    {
 0151                        localItems[key] = existing = (entity, []);
 152                    }
 153
 0154                    existing.ItemIds.Add(itemId);
 155                }
 156
 0157                foreach (var item in localItems)
 158                {
 0159                    operation.JellyfinDbContext.ItemValues.Add(item.Value.ItemValue);
 0160                    operation.JellyfinDbContext.ItemValuesMap.AddRange(item.Value.ItemIds.Distinct().Select(f => new Ite
 0161                    {
 0162                        Item = null!,
 0163                        ItemValue = null!,
 0164                        ItemId = f,
 0165                        ItemValueId = item.Value.ItemValue.ItemValueId
 0166                    }));
 167                }
 168            }
 169
 0170            using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.ItemValues.Local.Count} ItemValues ent
 171            {
 0172                operation.JellyfinDbContext.SaveChanges();
 0173            }
 174        }
 175
 0176        using (var operation = GetPreparedDbContext("moving UserData"))
 177        {
 0178            var queryResult = connection.Query(
 0179            """
 0180            SELECT key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStrea
 0181
 0182            WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.UserDataKey = UserDatas.key)
 0183            """);
 184
 0185            using (new TrackedMigrationStep("loading UserData", _logger))
 186            {
 0187                var users = operation.JellyfinDbContext.Users.AsNoTracking().ToImmutableArray();
 0188                var userIdBlacklist = new HashSet<int>();
 189
 0190                foreach (var entity in queryResult)
 191                {
 0192                    var userData = GetUserData(users, entity, userIdBlacklist);
 0193                    if (userData is null)
 194                    {
 0195                        var userDataId = entity.GetString(0);
 0196                        var internalUserId = entity.GetInt32(1);
 197
 0198                        if (!userIdBlacklist.Contains(internalUserId))
 199                        {
 0200                            _logger.LogError("Was not able to migrate user data with key {0} because its id {InternalId}
 0201                            userIdBlacklist.Add(internalUserId);
 202                        }
 203
 0204                        continue;
 205                    }
 206
 0207                    if (!legacyBaseItemWithUserKeys.TryGetValue(userData.CustomDataKey!, out var refItem))
 208                    {
 0209                        _logger.LogError("Was not able to migrate user data with key {0} because it does not reference a
 0210                        continue;
 211                    }
 212
 0213                    userData.ItemId = refItem.Id;
 0214                    operation.JellyfinDbContext.UserData.Add(userData);
 215                }
 216
 0217                users.Clear();
 0218            }
 219
 0220            legacyBaseItemWithUserKeys.Clear();
 221
 0222            using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.UserData.Local.Count} UserData entries
 223            {
 0224                operation.JellyfinDbContext.SaveChanges();
 0225            }
 226        }
 227
 0228        using (var operation = GetPreparedDbContext("moving MediaStreamInfos"))
 229        {
 230            const string mediaStreamQuery =
 231            """
 232            SELECT ItemId, StreamIndex, StreamType, Codec, Language, ChannelLayout, Profile, AspectRatio, Path,
 233            IsInterlaced, BitRate, Channels, SampleRate, IsDefault, IsForced, IsExternal, Height, Width,
 234            AverageFrameRate, RealFrameRate, Level, PixelFormat, BitDepth, IsAnamorphic, RefFrames, CodecTag,
 235            Comment, NalLengthSize, IsAvc, Title, TimeBase, CodecTimeBase, ColorPrimaries, ColorSpace, ColorTransfer,
 236            DvVersionMajor, DvVersionMinor, DvProfile, DvLevel, RpuPresentFlag, ElPresentFlag, BlPresentFlag, DvBlSignal
 237            FROM MediaStreams
 238            WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = MediaStreams.ItemId)
 239            """;
 240
 0241            using (new TrackedMigrationStep("loading MediaStreamInfos", _logger))
 242            {
 0243                foreach (SqliteDataReader dto in connection.Query(mediaStreamQuery))
 244                {
 0245                    operation.JellyfinDbContext.MediaStreamInfos.Add(GetMediaStream(dto));
 246                }
 247            }
 248
 0249            using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.MediaStreamInfos.Local.Count} MediaStr
 250            {
 0251                operation.JellyfinDbContext.SaveChanges();
 0252            }
 253        }
 254
 0255        using (var operation = GetPreparedDbContext("moving AttachmentStreamInfos"))
 256        {
 257            const string mediaAttachmentQuery =
 258            """
 259            SELECT ItemId, AttachmentIndex, Codec, CodecTag, Comment, filename, MIMEType
 260            FROM mediaattachments
 261            WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = mediaattachments.ItemId)
 262            """;
 263
 0264            using (new TrackedMigrationStep("loading AttachmentStreamInfos", _logger))
 265            {
 0266                foreach (SqliteDataReader dto in connection.Query(mediaAttachmentQuery))
 267                {
 0268                    operation.JellyfinDbContext.AttachmentStreamInfos.Add(GetMediaAttachment(dto));
 269                }
 270            }
 271
 0272            using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.AttachmentStreamInfos.Local.Count} Att
 273            {
 0274                operation.JellyfinDbContext.SaveChanges();
 0275            }
 276        }
 277
 0278        using (var operation = GetPreparedDbContext("moving People"))
 279        {
 280            const string personsQuery =
 281            """
 282            SELECT ItemId, Name, Role, PersonType, SortOrder FROM People
 283            WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = People.ItemId)
 284            """;
 285
 0286            var peopleCache = new Dictionary<string, (People Person, List<PeopleBaseItemMap> Items)>();
 287
 0288            using (new TrackedMigrationStep("loading People", _logger))
 289            {
 0290                foreach (SqliteDataReader reader in connection.Query(personsQuery))
 291                {
 0292                    var itemId = reader.GetGuid(0);
 0293                    if (!baseItemIds.Contains(itemId))
 294                    {
 0295                        _logger.LogError("Dont save person {0} because its not in use by any BaseItem", reader.GetString
 0296                        continue;
 297                    }
 298
 0299                    var entity = GetPerson(reader);
 0300                    if (!peopleCache.TryGetValue(entity.Name, out var personCache))
 301                    {
 0302                        peopleCache[entity.Name] = personCache = (entity, []);
 303                    }
 304
 0305                    if (reader.TryGetString(2, out var role))
 306                    {
 307                    }
 308
 0309                    int? sortOrder = reader.IsDBNull(4) ? null : reader.GetInt32(4);
 310
 0311                    personCache.Items.Add(new PeopleBaseItemMap()
 0312                    {
 0313                        Item = null!,
 0314                        ItemId = itemId,
 0315                        People = null!,
 0316                        PeopleId = personCache.Person.Id,
 0317                        ListOrder = sortOrder,
 0318                        SortOrder = sortOrder,
 0319                        Role = role
 0320                    });
 321                }
 322
 0323                baseItemIds.Clear();
 324
 0325                foreach (var item in peopleCache)
 326                {
 0327                    operation.JellyfinDbContext.Peoples.Add(item.Value.Person);
 0328                    operation.JellyfinDbContext.PeopleBaseItemMap.AddRange(item.Value.Items.DistinctBy(e => (e.ItemId, e
 329                }
 330
 0331                peopleCache.Clear();
 0332            }
 333
 0334            using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.Peoples.Local.Count} People entries an
 335            {
 0336                operation.JellyfinDbContext.SaveChanges();
 0337            }
 338        }
 339
 0340        using (var operation = GetPreparedDbContext("moving Chapters"))
 341        {
 342            const string chapterQuery =
 343            """
 344            SELECT ItemId,StartPositionTicks,Name,ImagePath,ImageDateModified,ChapterIndex from Chapters2
 345            WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = Chapters2.ItemId)
 346            """;
 347
 0348            using (new TrackedMigrationStep("loading Chapters", _logger))
 349            {
 0350                foreach (SqliteDataReader dto in connection.Query(chapterQuery))
 351                {
 0352                    var chapter = GetChapter(dto);
 0353                    operation.JellyfinDbContext.Chapters.Add(chapter);
 354                }
 355            }
 356
 0357            using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.Chapters.Local.Count} Chapters entries
 358            {
 0359                operation.JellyfinDbContext.SaveChanges();
 0360            }
 361        }
 362
 0363        using (var operation = GetPreparedDbContext("moving AncestorIds"))
 364        {
 365            const string ancestorIdsQuery =
 366            """
 367            SELECT ItemId, AncestorId, AncestorIdText FROM AncestorIds
 368            WHERE
 369            EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.ItemId)
 370            AND
 371            EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.AncestorId)
 372            """;
 373
 0374            using (new TrackedMigrationStep("loading AncestorIds", _logger))
 375            {
 0376                foreach (SqliteDataReader dto in connection.Query(ancestorIdsQuery))
 377                {
 0378                    var ancestorId = GetAncestorId(dto);
 0379                    operation.JellyfinDbContext.AncestorIds.Add(ancestorId);
 380                }
 381            }
 382
 0383            using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.AncestorIds.Local.Count} AncestorId en
 384            {
 0385                operation.JellyfinDbContext.SaveChanges();
 0386            }
 387        }
 388
 0389        connection.Close();
 390
 0391        _logger.LogInformation("Migration of the Library.db done.");
 0392        _logger.LogInformation("Migrating Library db took {0}.", fullOperationTimer.Elapsed);
 393
 0394        SqliteConnection.ClearAllPools();
 395
 0396        _logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old");
 0397        File.Move(libraryDbPath, libraryDbPath + ".old", true);
 398
 0399        _jellyfinDatabaseProvider.RunScheduledOptimisation(CancellationToken.None).ConfigureAwait(false).GetAwaiter().Ge
 0400    }
 401
 402    private DatabaseMigrationStep GetPreparedDbContext(string operationName)
 403    {
 0404        var dbContext = _provider.CreateDbContext();
 0405        dbContext.ChangeTracker.AutoDetectChangesEnabled = false;
 0406        dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
 0407        return new DatabaseMigrationStep(dbContext, operationName, _logger);
 408    }
 409
 410    private UserData? GetUserData(ImmutableArray<User> users, SqliteDataReader dto, HashSet<int> userIdBlacklist)
 411    {
 0412        var internalUserId = dto.GetInt32(1);
 0413        var user = users.FirstOrDefault(e => e.InternalId == internalUserId);
 414
 0415        if (user is null)
 416        {
 0417            if (userIdBlacklist.Contains(internalUserId))
 418            {
 0419                return null;
 420            }
 421
 0422            _logger.LogError("Tried to find user with index '{Idx}' but there are only '{MaxIdx}' users.", internalUserI
 0423            return null;
 424        }
 425
 0426        var oldKey = dto.GetString(0);
 427
 0428        return new UserData()
 0429        {
 0430            ItemId = Guid.NewGuid(),
 0431            CustomDataKey = oldKey,
 0432            UserId = user.Id,
 0433            Rating = dto.IsDBNull(2) ? null : dto.GetDouble(2),
 0434            Played = dto.GetBoolean(3),
 0435            PlayCount = dto.GetInt32(4),
 0436            IsFavorite = dto.GetBoolean(5),
 0437            PlaybackPositionTicks = dto.GetInt64(6),
 0438            LastPlayedDate = dto.IsDBNull(7) ? null : dto.GetDateTime(7),
 0439            AudioStreamIndex = dto.IsDBNull(8) ? null : dto.GetInt32(8),
 0440            SubtitleStreamIndex = dto.IsDBNull(9) ? null : dto.GetInt32(9),
 0441            Likes = null,
 0442            User = null!,
 0443            Item = null!
 0444        };
 445    }
 446
 447    private AncestorId GetAncestorId(SqliteDataReader reader)
 448    {
 0449        return new AncestorId()
 0450        {
 0451            ItemId = reader.GetGuid(0),
 0452            ParentItemId = reader.GetGuid(1),
 0453            Item = null!,
 0454            ParentItem = null!
 0455        };
 456    }
 457
 458    /// <summary>
 459    /// Gets the chapter.
 460    /// </summary>
 461    /// <param name="reader">The reader.</param>
 462    /// <returns>ChapterInfo.</returns>
 463    private Chapter GetChapter(SqliteDataReader reader)
 464    {
 0465        var chapter = new Chapter
 0466        {
 0467            StartPositionTicks = reader.GetInt64(1),
 0468            ChapterIndex = reader.GetInt32(5),
 0469            Item = null!,
 0470            ItemId = reader.GetGuid(0),
 0471        };
 472
 0473        if (reader.TryGetString(2, out var chapterName))
 474        {
 0475            chapter.Name = chapterName;
 476        }
 477
 0478        if (reader.TryGetString(3, out var imagePath))
 479        {
 0480            chapter.ImagePath = imagePath;
 481        }
 482
 0483        if (reader.TryReadDateTime(4, out var imageDateModified))
 484        {
 0485            chapter.ImageDateModified = imageDateModified;
 486        }
 487
 0488        return chapter;
 489    }
 490
 491    private ItemValue GetItemValue(SqliteDataReader reader)
 492    {
 0493        return new ItemValue
 0494        {
 0495            ItemValueId = Guid.NewGuid(),
 0496            Type = (ItemValueType)reader.GetInt32(1),
 0497            Value = reader.GetString(2),
 0498            CleanValue = reader.GetString(3),
 0499        };
 500    }
 501
 502    private People GetPerson(SqliteDataReader reader)
 503    {
 0504        var item = new People
 0505        {
 0506            Id = Guid.NewGuid(),
 0507            Name = reader.GetString(1),
 0508        };
 509
 0510        if (reader.TryGetString(3, out var type))
 511        {
 0512            item.PersonType = type;
 513        }
 514
 0515        return item;
 516    }
 517
 518    /// <summary>
 519    /// Gets the media stream.
 520    /// </summary>
 521    /// <param name="reader">The reader.</param>
 522    /// <returns>MediaStream.</returns>
 523    private MediaStreamInfo GetMediaStream(SqliteDataReader reader)
 524    {
 0525        var item = new MediaStreamInfo
 0526        {
 0527            StreamIndex = reader.GetInt32(1),
 0528            StreamType = Enum.Parse<MediaStreamTypeEntity>(reader.GetString(2)),
 0529            Item = null!,
 0530            ItemId = reader.GetGuid(0),
 0531            AspectRatio = null!,
 0532            ChannelLayout = null!,
 0533            Codec = null!,
 0534            IsInterlaced = false,
 0535            Language = null!,
 0536            Path = null!,
 0537            Profile = null!,
 0538        };
 539
 0540        if (reader.TryGetString(3, out var codec))
 541        {
 0542            item.Codec = codec;
 543        }
 544
 0545        if (reader.TryGetString(4, out var language))
 546        {
 0547            item.Language = language;
 548        }
 549
 0550        if (reader.TryGetString(5, out var channelLayout))
 551        {
 0552            item.ChannelLayout = channelLayout;
 553        }
 554
 0555        if (reader.TryGetString(6, out var profile))
 556        {
 0557            item.Profile = profile;
 558        }
 559
 0560        if (reader.TryGetString(7, out var aspectRatio))
 561        {
 0562            item.AspectRatio = aspectRatio;
 563        }
 564
 0565        if (reader.TryGetString(8, out var path))
 566        {
 0567            item.Path = path;
 568        }
 569
 0570        item.IsInterlaced = reader.GetBoolean(9);
 571
 0572        if (reader.TryGetInt32(10, out var bitrate))
 573        {
 0574            item.BitRate = bitrate;
 575        }
 576
 0577        if (reader.TryGetInt32(11, out var channels))
 578        {
 0579            item.Channels = channels;
 580        }
 581
 0582        if (reader.TryGetInt32(12, out var sampleRate))
 583        {
 0584            item.SampleRate = sampleRate;
 585        }
 586
 0587        item.IsDefault = reader.GetBoolean(13);
 0588        item.IsForced = reader.GetBoolean(14);
 0589        item.IsExternal = reader.GetBoolean(15);
 590
 0591        if (reader.TryGetInt32(16, out var width))
 592        {
 0593            item.Width = width;
 594        }
 595
 0596        if (reader.TryGetInt32(17, out var height))
 597        {
 0598            item.Height = height;
 599        }
 600
 0601        if (reader.TryGetSingle(18, out var averageFrameRate))
 602        {
 0603            item.AverageFrameRate = averageFrameRate;
 604        }
 605
 0606        if (reader.TryGetSingle(19, out var realFrameRate))
 607        {
 0608            item.RealFrameRate = realFrameRate;
 609        }
 610
 0611        if (reader.TryGetSingle(20, out var level))
 612        {
 0613            item.Level = level;
 614        }
 615
 0616        if (reader.TryGetString(21, out var pixelFormat))
 617        {
 0618            item.PixelFormat = pixelFormat;
 619        }
 620
 0621        if (reader.TryGetInt32(22, out var bitDepth))
 622        {
 0623            item.BitDepth = bitDepth;
 624        }
 625
 0626        if (reader.TryGetBoolean(23, out var isAnamorphic))
 627        {
 0628            item.IsAnamorphic = isAnamorphic;
 629        }
 630
 0631        if (reader.TryGetInt32(24, out var refFrames))
 632        {
 0633            item.RefFrames = refFrames;
 634        }
 635
 0636        if (reader.TryGetString(25, out var codecTag))
 637        {
 0638            item.CodecTag = codecTag;
 639        }
 640
 0641        if (reader.TryGetString(26, out var comment))
 642        {
 0643            item.Comment = comment;
 644        }
 645
 0646        if (reader.TryGetString(27, out var nalLengthSize))
 647        {
 0648            item.NalLengthSize = nalLengthSize;
 649        }
 650
 0651        if (reader.TryGetBoolean(28, out var isAVC))
 652        {
 0653            item.IsAvc = isAVC;
 654        }
 655
 0656        if (reader.TryGetString(29, out var title))
 657        {
 0658            item.Title = title;
 659        }
 660
 0661        if (reader.TryGetString(30, out var timeBase))
 662        {
 0663            item.TimeBase = timeBase;
 664        }
 665
 0666        if (reader.TryGetString(31, out var codecTimeBase))
 667        {
 0668            item.CodecTimeBase = codecTimeBase;
 669        }
 670
 0671        if (reader.TryGetString(32, out var colorPrimaries))
 672        {
 0673            item.ColorPrimaries = colorPrimaries;
 674        }
 675
 0676        if (reader.TryGetString(33, out var colorSpace))
 677        {
 0678            item.ColorSpace = colorSpace;
 679        }
 680
 0681        if (reader.TryGetString(34, out var colorTransfer))
 682        {
 0683            item.ColorTransfer = colorTransfer;
 684        }
 685
 0686        if (reader.TryGetInt32(35, out var dvVersionMajor))
 687        {
 0688            item.DvVersionMajor = dvVersionMajor;
 689        }
 690
 0691        if (reader.TryGetInt32(36, out var dvVersionMinor))
 692        {
 0693            item.DvVersionMinor = dvVersionMinor;
 694        }
 695
 0696        if (reader.TryGetInt32(37, out var dvProfile))
 697        {
 0698            item.DvProfile = dvProfile;
 699        }
 700
 0701        if (reader.TryGetInt32(38, out var dvLevel))
 702        {
 0703            item.DvLevel = dvLevel;
 704        }
 705
 0706        if (reader.TryGetInt32(39, out var rpuPresentFlag))
 707        {
 0708            item.RpuPresentFlag = rpuPresentFlag;
 709        }
 710
 0711        if (reader.TryGetInt32(40, out var elPresentFlag))
 712        {
 0713            item.ElPresentFlag = elPresentFlag;
 714        }
 715
 0716        if (reader.TryGetInt32(41, out var blPresentFlag))
 717        {
 0718            item.BlPresentFlag = blPresentFlag;
 719        }
 720
 0721        if (reader.TryGetInt32(42, out var dvBlSignalCompatibilityId))
 722        {
 0723            item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId;
 724        }
 725
 0726        item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result;
 727
 728        // if (reader.TryGetInt32(44, out var rotation))
 729        // {
 730        //     item.Rotation = rotation;
 731        // }
 732
 0733        return item;
 734    }
 735
 736    /// <summary>
 737    /// Gets the attachment.
 738    /// </summary>
 739    /// <param name="reader">The reader.</param>
 740    /// <returns>MediaAttachment.</returns>
 741    private AttachmentStreamInfo GetMediaAttachment(SqliteDataReader reader)
 742    {
 0743        var item = new AttachmentStreamInfo
 0744        {
 0745            Index = reader.GetInt32(1),
 0746            Item = null!,
 0747            ItemId = reader.GetGuid(0),
 0748        };
 749
 0750        if (reader.TryGetString(2, out var codec))
 751        {
 0752            item.Codec = codec;
 753        }
 754
 0755        if (reader.TryGetString(3, out var codecTag))
 756        {
 0757            item.CodecTag = codecTag;
 758        }
 759
 0760        if (reader.TryGetString(4, out var comment))
 761        {
 0762            item.Comment = comment;
 763        }
 764
 0765        if (reader.TryGetString(5, out var fileName))
 766        {
 0767            item.Filename = fileName;
 768        }
 769
 0770        if (reader.TryGetString(6, out var mimeType))
 771        {
 0772            item.MimeType = mimeType;
 773        }
 774
 0775        return item;
 776    }
 777
 778    private (BaseItemEntity BaseItem, string[] LegacyUserDataKey) GetItem(SqliteDataReader reader)
 779    {
 0780        var entity = new BaseItemEntity()
 0781        {
 0782            Id = reader.GetGuid(0),
 0783            Type = reader.GetString(1),
 0784        };
 785
 0786        var index = 2;
 787
 0788        if (reader.TryGetString(index++, out var data))
 789        {
 0790            entity.Data = data;
 791        }
 792
 0793        if (reader.TryReadDateTime(index++, out var startDate))
 794        {
 0795            entity.StartDate = startDate;
 796        }
 797
 0798        if (reader.TryReadDateTime(index++, out var endDate))
 799        {
 0800            entity.EndDate = endDate;
 801        }
 802
 0803        if (reader.TryGetGuid(index++, out var guid))
 804        {
 0805            entity.ChannelId = guid;
 806        }
 807
 0808        if (reader.TryGetBoolean(index++, out var isMovie))
 809        {
 0810            entity.IsMovie = isMovie;
 811        }
 812
 0813        if (reader.TryGetBoolean(index++, out var isSeries))
 814        {
 0815            entity.IsSeries = isSeries;
 816        }
 817
 0818        if (reader.TryGetString(index++, out var episodeTitle))
 819        {
 0820            entity.EpisodeTitle = episodeTitle;
 821        }
 822
 0823        if (reader.TryGetBoolean(index++, out var isRepeat))
 824        {
 0825            entity.IsRepeat = isRepeat;
 826        }
 827
 0828        if (reader.TryGetSingle(index++, out var communityRating))
 829        {
 0830            entity.CommunityRating = communityRating;
 831        }
 832
 0833        if (reader.TryGetString(index++, out var customRating))
 834        {
 0835            entity.CustomRating = customRating;
 836        }
 837
 0838        if (reader.TryGetInt32(index++, out var indexNumber))
 839        {
 0840            entity.IndexNumber = indexNumber;
 841        }
 842
 0843        if (reader.TryGetBoolean(index++, out var isLocked))
 844        {
 0845            entity.IsLocked = isLocked;
 846        }
 847
 0848        if (reader.TryGetString(index++, out var preferredMetadataLanguage))
 849        {
 0850            entity.PreferredMetadataLanguage = preferredMetadataLanguage;
 851        }
 852
 0853        if (reader.TryGetString(index++, out var preferredMetadataCountryCode))
 854        {
 0855            entity.PreferredMetadataCountryCode = preferredMetadataCountryCode;
 856        }
 857
 0858        if (reader.TryGetInt32(index++, out var width))
 859        {
 0860            entity.Width = width;
 861        }
 862
 0863        if (reader.TryGetInt32(index++, out var height))
 864        {
 0865            entity.Height = height;
 866        }
 867
 0868        if (reader.TryReadDateTime(index++, out var dateLastRefreshed))
 869        {
 0870            entity.DateLastRefreshed = dateLastRefreshed;
 871        }
 872
 0873        if (reader.TryGetString(index++, out var name))
 874        {
 0875            entity.Name = name;
 876        }
 877
 0878        if (reader.TryGetString(index++, out var restorePath))
 879        {
 0880            entity.Path = restorePath;
 881        }
 882
 0883        if (reader.TryReadDateTime(index++, out var premiereDate))
 884        {
 0885            entity.PremiereDate = premiereDate;
 886        }
 887
 0888        if (reader.TryGetString(index++, out var overview))
 889        {
 0890            entity.Overview = overview;
 891        }
 892
 0893        if (reader.TryGetInt32(index++, out var parentIndexNumber))
 894        {
 0895            entity.ParentIndexNumber = parentIndexNumber;
 896        }
 897
 0898        if (reader.TryGetInt32(index++, out var productionYear))
 899        {
 0900            entity.ProductionYear = productionYear;
 901        }
 902
 0903        if (reader.TryGetString(index++, out var officialRating))
 904        {
 0905            entity.OfficialRating = officialRating;
 906        }
 907
 0908        if (reader.TryGetString(index++, out var forcedSortName))
 909        {
 0910            entity.ForcedSortName = forcedSortName;
 911        }
 912
 0913        if (reader.TryGetInt64(index++, out var runTimeTicks))
 914        {
 0915            entity.RunTimeTicks = runTimeTicks;
 916        }
 917
 0918        if (reader.TryGetInt64(index++, out var size))
 919        {
 0920            entity.Size = size;
 921        }
 922
 0923        if (reader.TryReadDateTime(index++, out var dateCreated))
 924        {
 0925            entity.DateCreated = dateCreated;
 926        }
 927
 0928        if (reader.TryReadDateTime(index++, out var dateModified))
 929        {
 0930            entity.DateModified = dateModified;
 931        }
 932
 0933        if (reader.TryGetString(index++, out var genres))
 934        {
 0935            entity.Genres = genres;
 936        }
 937
 0938        if (reader.TryGetGuid(index++, out var parentId))
 939        {
 0940            entity.ParentId = parentId;
 941        }
 942
 0943        if (reader.TryGetGuid(index++, out var topParentId))
 944        {
 0945            entity.TopParentId = topParentId;
 946        }
 947
 0948        if (reader.TryGetString(index++, out var audioString) && Enum.TryParse<ProgramAudioEntity>(audioString, out var 
 949        {
 0950            entity.Audio = audioType;
 951        }
 952
 0953        if (reader.TryGetString(index++, out var serviceName))
 954        {
 0955            entity.ExternalServiceId = serviceName;
 956        }
 957
 0958        if (reader.TryGetBoolean(index++, out var isInMixedFolder))
 959        {
 0960            entity.IsInMixedFolder = isInMixedFolder;
 961        }
 962
 0963        if (reader.TryReadDateTime(index++, out var dateLastSaved))
 964        {
 0965            entity.DateLastSaved = dateLastSaved;
 966        }
 967
 0968        if (reader.TryGetString(index++, out var lockedFields))
 969        {
 0970            entity.LockedFields = lockedFields.Split('|').Select(Enum.Parse<MetadataField>)
 0971                .Select(e => new BaseItemMetadataField()
 0972                {
 0973                    Id = (int)e,
 0974                    Item = entity,
 0975                    ItemId = entity.Id
 0976                })
 0977                .ToArray();
 978        }
 979
 0980        if (reader.TryGetString(index++, out var studios))
 981        {
 0982            entity.Studios = studios;
 983        }
 984
 0985        if (reader.TryGetString(index++, out var tags))
 986        {
 0987            entity.Tags = tags;
 988        }
 989
 0990        if (reader.TryGetString(index++, out var trailerTypes))
 991        {
 0992            entity.TrailerTypes = trailerTypes.Split('|').Select(Enum.Parse<TrailerType>)
 0993                .Select(e => new BaseItemTrailerType()
 0994                {
 0995                    Id = (int)e,
 0996                    Item = entity,
 0997                    ItemId = entity.Id
 0998                })
 0999                .ToArray();
 1000        }
 1001
 01002        if (reader.TryGetString(index++, out var originalTitle))
 1003        {
 01004            entity.OriginalTitle = originalTitle;
 1005        }
 1006
 01007        if (reader.TryGetString(index++, out var primaryVersionId))
 1008        {
 01009            entity.PrimaryVersionId = primaryVersionId;
 1010        }
 1011
 01012        if (reader.TryReadDateTime(index++, out var dateLastMediaAdded))
 1013        {
 01014            entity.DateLastMediaAdded = dateLastMediaAdded;
 1015        }
 1016
 01017        if (reader.TryGetString(index++, out var album))
 1018        {
 01019            entity.Album = album;
 1020        }
 1021
 01022        if (reader.TryGetSingle(index++, out var lUFS))
 1023        {
 01024            entity.LUFS = lUFS;
 1025        }
 1026
 01027        if (reader.TryGetSingle(index++, out var normalizationGain))
 1028        {
 01029            entity.NormalizationGain = normalizationGain;
 1030        }
 1031
 01032        if (reader.TryGetSingle(index++, out var criticRating))
 1033        {
 01034            entity.CriticRating = criticRating;
 1035        }
 1036
 01037        if (reader.TryGetBoolean(index++, out var isVirtualItem))
 1038        {
 01039            entity.IsVirtualItem = isVirtualItem;
 1040        }
 1041
 01042        if (reader.TryGetString(index++, out var seriesName))
 1043        {
 01044            entity.SeriesName = seriesName;
 1045        }
 1046
 01047        var userDataKeys = new List<string>();
 01048        if (reader.TryGetString(index++, out var directUserDataKey))
 1049        {
 01050            userDataKeys.Add(directUserDataKey);
 1051        }
 1052
 01053        if (reader.TryGetString(index++, out var seasonName))
 1054        {
 01055            entity.SeasonName = seasonName;
 1056        }
 1057
 01058        if (reader.TryGetGuid(index++, out var seasonId))
 1059        {
 01060            entity.SeasonId = seasonId;
 1061        }
 1062
 01063        if (reader.TryGetGuid(index++, out var seriesId))
 1064        {
 01065            entity.SeriesId = seriesId;
 1066        }
 1067
 01068        if (reader.TryGetString(index++, out var presentationUniqueKey))
 1069        {
 01070            entity.PresentationUniqueKey = presentationUniqueKey;
 1071        }
 1072
 01073        if (reader.TryGetInt32(index++, out var parentalRating))
 1074        {
 01075            entity.InheritedParentalRatingValue = parentalRating;
 1076        }
 1077
 01078        if (reader.TryGetString(index++, out var externalSeriesId))
 1079        {
 01080            entity.ExternalSeriesId = externalSeriesId;
 1081        }
 1082
 01083        if (reader.TryGetString(index++, out var tagLine))
 1084        {
 01085            entity.Tagline = tagLine;
 1086        }
 1087
 01088        if (reader.TryGetString(index++, out var providerIds))
 1089        {
 01090            entity.Provider = providerIds.Split('|').Select(e => e.Split("="))
 01091            .Select(e => new BaseItemProvider()
 01092            {
 01093                Item = null!,
 01094                ProviderId = e[0],
 01095                ProviderValue = e[1]
 01096            }).ToArray();
 1097        }
 1098
 01099        if (reader.TryGetString(index++, out var imageInfos))
 1100        {
 01101            entity.Images = DeserializeImages(imageInfos).Select(f => Map(entity.Id, f)).ToArray();
 1102        }
 1103
 01104        if (reader.TryGetString(index++, out var productionLocations))
 1105        {
 01106            entity.ProductionLocations = productionLocations;
 1107        }
 1108
 01109        if (reader.TryGetString(index++, out var extraIds))
 1110        {
 01111            entity.ExtraIds = extraIds;
 1112        }
 1113
 01114        if (reader.TryGetInt32(index++, out var totalBitrate))
 1115        {
 01116            entity.TotalBitrate = totalBitrate;
 1117        }
 1118
 01119        if (reader.TryGetString(index++, out var extraTypeString) && Enum.TryParse<BaseItemExtraType>(extraTypeString, o
 1120        {
 01121            entity.ExtraType = extraType;
 1122        }
 1123
 01124        if (reader.TryGetString(index++, out var artists))
 1125        {
 01126            entity.Artists = artists;
 1127        }
 1128
 01129        if (reader.TryGetString(index++, out var albumArtists))
 1130        {
 01131            entity.AlbumArtists = albumArtists;
 1132        }
 1133
 01134        if (reader.TryGetString(index++, out var externalId))
 1135        {
 01136            entity.ExternalId = externalId;
 1137        }
 1138
 01139        if (reader.TryGetString(index++, out var seriesPresentationUniqueKey))
 1140        {
 01141            entity.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
 1142        }
 1143
 01144        if (reader.TryGetString(index++, out var showId))
 1145        {
 01146            entity.ShowId = showId;
 1147        }
 1148
 01149        if (reader.TryGetString(index++, out var ownerId))
 1150        {
 01151            entity.OwnerId = ownerId;
 1152        }
 1153
 01154        if (reader.TryGetString(index++, out var mediaType))
 1155        {
 01156            entity.MediaType = mediaType;
 1157        }
 1158
 01159        if (reader.TryGetString(index++, out var sortName))
 1160        {
 01161            entity.SortName = sortName;
 1162        }
 1163
 01164        if (reader.TryGetString(index++, out var cleanName))
 1165        {
 01166            entity.CleanName = cleanName;
 1167        }
 1168
 01169        if (reader.TryGetString(index++, out var unratedType))
 1170        {
 01171            entity.UnratedType = unratedType;
 1172        }
 1173
 01174        var baseItem = BaseItemRepository.DeserialiseBaseItem(entity, _logger, null, false);
 01175        var dataKeys = baseItem.GetUserDataKeys();
 01176        userDataKeys.AddRange(dataKeys);
 1177
 01178        return (entity, userDataKeys.ToArray());
 1179    }
 1180
 1181    private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
 1182    {
 01183        return new BaseItemImageInfo()
 01184        {
 01185            ItemId = baseItemId,
 01186            Id = Guid.NewGuid(),
 01187            Path = e.Path,
 01188            Blurhash = e.BlurHash != null ? Encoding.UTF8.GetBytes(e.BlurHash) : null,
 01189            DateModified = e.DateModified,
 01190            Height = e.Height,
 01191            Width = e.Width,
 01192            ImageType = (ImageInfoImageType)e.Type,
 01193            Item = null!
 01194        };
 1195    }
 1196
 1197    internal ItemImageInfo[] DeserializeImages(string value)
 1198    {
 01199        if (string.IsNullOrWhiteSpace(value))
 1200        {
 01201            return Array.Empty<ItemImageInfo>();
 1202        }
 1203
 1204        // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the data
 01205        var valueSpan = value.AsSpan();
 01206        var count = valueSpan.Count('|') + 1;
 1207
 01208        var position = 0;
 01209        var result = new ItemImageInfo[count];
 01210        foreach (var part in valueSpan.Split('|'))
 1211        {
 01212            var image = ItemImageInfoFromValueString(part);
 1213
 01214            if (image is not null)
 1215            {
 01216                result[position++] = image;
 1217            }
 1218        }
 1219
 01220        if (position == count)
 1221        {
 01222            return result;
 1223        }
 1224
 01225        if (position == 0)
 1226        {
 01227            return Array.Empty<ItemImageInfo>();
 1228        }
 1229
 1230        // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array.
 01231        return result[..position];
 1232    }
 1233
 1234    internal ItemImageInfo? ItemImageInfoFromValueString(ReadOnlySpan<char> value)
 1235    {
 1236        const char Delimiter = '*';
 1237
 01238        var nextSegment = value.IndexOf(Delimiter);
 01239        if (nextSegment == -1)
 1240        {
 01241            return null;
 1242        }
 1243
 01244        ReadOnlySpan<char> path = value[..nextSegment];
 01245        value = value[(nextSegment + 1)..];
 01246        nextSegment = value.IndexOf(Delimiter);
 01247        if (nextSegment == -1)
 1248        {
 01249            return null;
 1250        }
 1251
 01252        ReadOnlySpan<char> dateModified = value[..nextSegment];
 01253        value = value[(nextSegment + 1)..];
 01254        nextSegment = value.IndexOf(Delimiter);
 01255        if (nextSegment == -1)
 1256        {
 01257            nextSegment = value.Length;
 1258        }
 1259
 01260        ReadOnlySpan<char> imageType = value[..nextSegment];
 1261
 01262        var image = new ItemImageInfo
 01263        {
 01264            Path = path.ToString()
 01265        };
 1266
 01267        if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks)
 01268            && ticks >= DateTime.MinValue.Ticks
 01269            && ticks <= DateTime.MaxValue.Ticks)
 1270        {
 01271            image.DateModified = new DateTime(ticks, DateTimeKind.Utc);
 1272        }
 1273        else
 1274        {
 01275            return null;
 1276        }
 1277
 01278        if (Enum.TryParse(imageType, true, out ImageType type))
 1279        {
 01280            image.Type = type;
 1281        }
 1282        else
 1283        {
 01284            return null;
 1285        }
 1286
 1287        // Optional parameters: width*height*blurhash
 01288        if (nextSegment + 1 < value.Length - 1)
 1289        {
 01290            value = value[(nextSegment + 1)..];
 01291            nextSegment = value.IndexOf(Delimiter);
 01292            if (nextSegment == -1 || nextSegment == value.Length)
 1293            {
 01294                return image;
 1295            }
 1296
 01297            ReadOnlySpan<char> widthSpan = value[..nextSegment];
 1298
 01299            value = value[(nextSegment + 1)..];
 01300            nextSegment = value.IndexOf(Delimiter);
 01301            if (nextSegment == -1)
 1302            {
 01303                nextSegment = value.Length;
 1304            }
 1305
 01306            ReadOnlySpan<char> heightSpan = value[..nextSegment];
 1307
 01308            if (int.TryParse(widthSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var width)
 01309                && int.TryParse(heightSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var height))
 1310            {
 01311                image.Width = width;
 01312                image.Height = height;
 1313            }
 1314
 01315            if (nextSegment < value.Length - 1)
 1316            {
 01317                value = value[(nextSegment + 1)..];
 01318                var length = value.Length;
 1319
 01320                Span<char> blurHashSpan = stackalloc char[length];
 01321                for (int i = 0; i < length; i++)
 1322                {
 01323                    var c = value[i];
 01324                    blurHashSpan[i] = c switch
 01325                    {
 01326                        '/' => Delimiter,
 01327                        '\\' => '|',
 01328                        _ => c
 01329                    };
 1330                }
 1331
 01332                image.BlurHash = new string(blurHashSpan);
 1333            }
 1334        }
 1335
 01336        return image;
 1337    }
 1338
 1339    private class TrackedMigrationStep : IDisposable
 1340    {
 1341        private readonly string _operationName;
 1342        private readonly ILogger _logger;
 1343        private readonly Stopwatch _operationTimer;
 1344        private bool _disposed;
 1345
 1346        public TrackedMigrationStep(string operationName, ILogger logger)
 1347        {
 01348            _operationName = operationName;
 01349            _logger = logger;
 01350            _operationTimer = Stopwatch.StartNew();
 01351            logger.LogInformation("Start {OperationName}", operationName);
 01352        }
 1353
 1354        public bool Disposed
 1355        {
 01356            get => _disposed;
 01357            set => _disposed = value;
 1358        }
 1359
 1360        public virtual void Dispose()
 1361        {
 01362            if (Disposed)
 1363            {
 01364                return;
 1365            }
 1366
 01367            Disposed = true;
 01368            _logger.LogInformation("{OperationName} took '{Time}'", _operationName, _operationTimer.Elapsed);
 01369        }
 1370    }
 1371
 1372    private sealed class DatabaseMigrationStep : TrackedMigrationStep
 1373    {
 01374        public DatabaseMigrationStep(JellyfinDbContext jellyfinDbContext, string operationName, ILogger logger) : base(o
 1375        {
 1376            JellyfinDbContext = jellyfinDbContext;
 01377        }
 1378
 1379        public JellyfinDbContext JellyfinDbContext { get; }
 1380
 1381        public override void Dispose()
 1382        {
 01383            if (Disposed)
 1384            {
 01385                return;
 1386            }
 1387
 01388            JellyfinDbContext.Dispose();
 01389            base.Dispose();
 01390        }
 1391    }
 1392}