< 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: 614
Coverable lines: 614
Total lines: 1396
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 342
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%232561520%
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 Jellyfin.Server.ServerSetupApp;
 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>
 32[JellyfinMigration("2025-04-20T20:00:00", nameof(MigrateLibraryDb))]
 33[JellyfinMigrationBackup(JellyfinDb = true, LegacyLibraryDb = true)]
 34internal class MigrateLibraryDb : IDatabaseMigrationRoutine
 35{
 36    private const string DbFilename = "library.db";
 37
 38    private readonly IStartupLogger _logger;
 39    private readonly IServerApplicationPaths _paths;
 40    private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider;
 41    private readonly IDbContextFactory<JellyfinDbContext> _provider;
 42
 43    /// <summary>
 44    /// Initializes a new instance of the <see cref="MigrateLibraryDb"/> class.
 45    /// </summary>
 46    /// <param name="startupLogger">The startup logger for Startup UI intigration.</param>
 47    /// <param name="provider">The database provider.</param>
 48    /// <param name="paths">The server application paths.</param>
 49    /// <param name="jellyfinDatabaseProvider">The database provider for special access.</param>
 50    public MigrateLibraryDb(
 51        IStartupLogger<MigrateLibraryDb> startupLogger,
 52        IDbContextFactory<JellyfinDbContext> provider,
 53        IServerApplicationPaths paths,
 54        IJellyfinDatabaseProvider jellyfinDatabaseProvider)
 55    {
 056        _logger = startupLogger;
 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
 93        // notify the other migration to just silently abort because the fix has been applied here already.
 094        ReseedFolderFlag.RerunGuardFlag = true;
 95
 096        var legacyBaseItemWithUserKeys = new Dictionary<string, BaseItemEntity>();
 097        connection.Open();
 98
 099        var baseItemIds = new HashSet<Guid>();
 0100        using (var operation = GetPreparedDbContext("Moving TypedBaseItem"))
 101        {
 102            const string typedBaseItemsQuery =
 103            """
 104            SELECT guid, type, data, StartDate, EndDate, ChannelId, IsMovie,
 105            IsSeries, EpisodeTitle, IsRepeat, CommunityRating, CustomRating, IndexNumber, IsLocked, PreferredMetadataLan
 106            PreferredMetadataCountryCode, Width, Height, DateLastRefreshed, Name, Path, PremiereDate, Overview, ParentIn
 107            ProductionYear, OfficialRating, ForcedSortName, RunTimeTicks, Size, DateCreated, DateModified, Genres, Paren
 108            Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, Origina
 109            DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, Se
 110            PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, Product
 111            ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortN
 112            """;
 0113            using (new TrackedMigrationStep("Loading TypedBaseItems", _logger))
 114            {
 0115                foreach (SqliteDataReader dto in connection.Query(typedBaseItemsQuery))
 116                {
 0117                    var baseItem = GetItem(dto);
 0118                    operation.JellyfinDbContext.BaseItems.Add(baseItem.BaseItem);
 0119                    baseItemIds.Add(baseItem.BaseItem.Id);
 0120                    foreach (var dataKey in baseItem.LegacyUserDataKey)
 121                    {
 0122                        legacyBaseItemWithUserKeys[dataKey] = baseItem.BaseItem;
 123                    }
 124                }
 125            }
 126
 0127            using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.BaseItems.Local.Count} BaseItem entrie
 128            {
 0129                operation.JellyfinDbContext.SaveChanges();
 0130            }
 131        }
 132
 0133        using (var operation = GetPreparedDbContext("Moving ItemValues"))
 134        {
 135            // do not migrate inherited types as they are now properly mapped in search and lookup.
 136            const string itemValueQuery =
 137            """
 138            SELECT ItemId, Type, Value, CleanValue FROM ItemValues
 139                        WHERE Type <> 6 AND EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = ItemValues.I
 140            """;
 141
 142            // EFCores local lookup sucks. We cannot use context.ItemValues.Local here because its just super slow.
 0143            var localItems = new Dictionary<(int Type, string Value), (Database.Implementations.Entities.ItemValue ItemV
 0144            using (new TrackedMigrationStep("Loading ItemValues", _logger))
 145            {
 0146                foreach (SqliteDataReader dto in connection.Query(itemValueQuery))
 147                {
 0148                    var itemId = dto.GetGuid(0);
 0149                    var entity = GetItemValue(dto);
 0150                    var key = ((int)entity.Type, entity.Value);
 0151                    if (!localItems.TryGetValue(key, out var existing))
 152                    {
 0153                        localItems[key] = existing = (entity, []);
 154                    }
 155
 0156                    existing.ItemIds.Add(itemId);
 157                }
 158
 0159                foreach (var item in localItems)
 160                {
 0161                    operation.JellyfinDbContext.ItemValues.Add(item.Value.ItemValue);
 0162                    operation.JellyfinDbContext.ItemValuesMap.AddRange(item.Value.ItemIds.Distinct().Select(f => new Ite
 0163                    {
 0164                        Item = null!,
 0165                        ItemValue = null!,
 0166                        ItemId = f,
 0167                        ItemValueId = item.Value.ItemValue.ItemValueId
 0168                    }));
 169                }
 170            }
 171
 0172            using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.ItemValues.Local.Count} ItemValues ent
 173            {
 0174                operation.JellyfinDbContext.SaveChanges();
 0175            }
 176        }
 177
 0178        using (var operation = GetPreparedDbContext("Moving UserData"))
 179        {
 0180            var queryResult = connection.Query(
 0181            """
 0182            SELECT key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStrea
 0183
 0184            WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.UserDataKey = UserDatas.key)
 0185            """);
 186
 0187            using (new TrackedMigrationStep("Loading UserData", _logger))
 188            {
 0189                var users = operation.JellyfinDbContext.Users.AsNoTracking().ToArray();
 0190                var userIdBlacklist = new HashSet<int>();
 191
 0192                foreach (var entity in queryResult)
 193                {
 0194                    var userData = GetUserData(users, entity, userIdBlacklist, _logger);
 0195                    if (userData is null)
 196                    {
 0197                        var userDataId = entity.GetString(0);
 0198                        var internalUserId = entity.GetInt32(1);
 199
 0200                        if (!userIdBlacklist.Contains(internalUserId))
 201                        {
 0202                            _logger.LogError("Was not able to migrate user data with key {0} because its id {InternalId}
 0203                            userIdBlacklist.Add(internalUserId);
 204                        }
 205
 0206                        continue;
 207                    }
 208
 0209                    if (!legacyBaseItemWithUserKeys.TryGetValue(userData.CustomDataKey!, out var refItem))
 210                    {
 0211                        _logger.LogError("Was not able to migrate user data with key {0} because it does not reference a
 0212                        continue;
 213                    }
 214
 0215                    userData.ItemId = refItem.Id;
 0216                    operation.JellyfinDbContext.UserData.Add(userData);
 217                }
 218            }
 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("Not saving person {0} because it's not in use by any BaseItem", reader.GetStri
 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);
 0398    }
 399
 400    private DatabaseMigrationStep GetPreparedDbContext(string operationName)
 401    {
 0402        var dbContext = _provider.CreateDbContext();
 0403        dbContext.ChangeTracker.AutoDetectChangesEnabled = false;
 0404        dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
 0405        return new DatabaseMigrationStep(dbContext, operationName, _logger);
 406    }
 407
 408    internal static UserData? GetUserData(User[] users, SqliteDataReader dto, HashSet<int> userIdBlacklist, ILogger logg
 409    {
 0410        var internalUserId = dto.GetInt32(1);
 0411        if (userIdBlacklist.Contains(internalUserId))
 412        {
 0413            return null;
 414        }
 415
 0416        var user = users.FirstOrDefault(e => e.InternalId == internalUserId);
 0417        if (user is null)
 418        {
 0419            userIdBlacklist.Add(internalUserId);
 420
 0421            logger.LogError("Tried to find user with index '{Idx}' but there are only '{MaxIdx}' users.", internalUserId
 0422            return null;
 423        }
 424
 0425        var oldKey = dto.GetString(0);
 426
 0427        return new UserData()
 0428        {
 0429            ItemId = Guid.NewGuid(),
 0430            CustomDataKey = oldKey,
 0431            UserId = user.Id,
 0432            Rating = dto.IsDBNull(2) ? null : dto.GetDouble(2),
 0433            Played = dto.GetBoolean(3),
 0434            PlayCount = dto.GetInt32(4),
 0435            IsFavorite = dto.GetBoolean(5),
 0436            PlaybackPositionTicks = dto.GetInt64(6),
 0437            LastPlayedDate = dto.IsDBNull(7) ? null : dto.GetDateTime(7),
 0438            AudioStreamIndex = dto.IsDBNull(8) ? null : dto.GetInt32(8),
 0439            SubtitleStreamIndex = dto.IsDBNull(9) ? null : dto.GetInt32(9),
 0440            Likes = null,
 0441            User = null!,
 0442            Item = null!
 0443        };
 444    }
 445
 446    private AncestorId GetAncestorId(SqliteDataReader reader)
 447    {
 0448        return new AncestorId()
 0449        {
 0450            ItemId = reader.GetGuid(0),
 0451            ParentItemId = reader.GetGuid(1),
 0452            Item = null!,
 0453            ParentItem = null!
 0454        };
 455    }
 456
 457    /// <summary>
 458    /// Gets the chapter.
 459    /// </summary>
 460    /// <param name="reader">The reader.</param>
 461    /// <returns>ChapterInfo.</returns>
 462    private Chapter GetChapter(SqliteDataReader reader)
 463    {
 0464        var chapter = new Chapter
 0465        {
 0466            StartPositionTicks = reader.GetInt64(1),
 0467            ChapterIndex = reader.GetInt32(5),
 0468            Item = null!,
 0469            ItemId = reader.GetGuid(0),
 0470        };
 471
 0472        if (reader.TryGetString(2, out var chapterName))
 473        {
 0474            chapter.Name = chapterName;
 475        }
 476
 0477        if (reader.TryGetString(3, out var imagePath))
 478        {
 0479            chapter.ImagePath = imagePath;
 480        }
 481
 0482        if (reader.TryReadDateTime(4, out var imageDateModified))
 483        {
 0484            chapter.ImageDateModified = imageDateModified;
 485        }
 486
 0487        return chapter;
 488    }
 489
 490    private ItemValue GetItemValue(SqliteDataReader reader)
 491    {
 0492        return new ItemValue
 0493        {
 0494            ItemValueId = Guid.NewGuid(),
 0495            Type = (ItemValueType)reader.GetInt32(1),
 0496            Value = reader.GetString(2),
 0497            CleanValue = reader.GetString(3),
 0498        };
 499    }
 500
 501    private People GetPerson(SqliteDataReader reader)
 502    {
 0503        var item = new People
 0504        {
 0505            Id = Guid.NewGuid(),
 0506            Name = reader.GetString(1),
 0507        };
 508
 0509        if (reader.TryGetString(3, out var type))
 510        {
 0511            item.PersonType = type;
 512        }
 513
 0514        return item;
 515    }
 516
 517    /// <summary>
 518    /// Gets the media stream.
 519    /// </summary>
 520    /// <param name="reader">The reader.</param>
 521    /// <returns>MediaStream.</returns>
 522    private MediaStreamInfo GetMediaStream(SqliteDataReader reader)
 523    {
 0524        var item = new MediaStreamInfo
 0525        {
 0526            StreamIndex = reader.GetInt32(1),
 0527            StreamType = Enum.Parse<MediaStreamTypeEntity>(reader.GetString(2)),
 0528            Item = null!,
 0529            ItemId = reader.GetGuid(0),
 0530            AspectRatio = null!,
 0531            ChannelLayout = null!,
 0532            Codec = null!,
 0533            IsInterlaced = false,
 0534            Language = null!,
 0535            Path = null!,
 0536            Profile = null!,
 0537        };
 538
 0539        if (reader.TryGetString(3, out var codec))
 540        {
 0541            item.Codec = codec;
 542        }
 543
 0544        if (reader.TryGetString(4, out var language))
 545        {
 0546            item.Language = language;
 547        }
 548
 0549        if (reader.TryGetString(5, out var channelLayout))
 550        {
 0551            item.ChannelLayout = channelLayout;
 552        }
 553
 0554        if (reader.TryGetString(6, out var profile))
 555        {
 0556            item.Profile = profile;
 557        }
 558
 0559        if (reader.TryGetString(7, out var aspectRatio))
 560        {
 0561            item.AspectRatio = aspectRatio;
 562        }
 563
 0564        if (reader.TryGetString(8, out var path))
 565        {
 0566            item.Path = path;
 567        }
 568
 0569        item.IsInterlaced = reader.GetBoolean(9);
 570
 0571        if (reader.TryGetInt32(10, out var bitrate))
 572        {
 0573            item.BitRate = bitrate;
 574        }
 575
 0576        if (reader.TryGetInt32(11, out var channels))
 577        {
 0578            item.Channels = channels;
 579        }
 580
 0581        if (reader.TryGetInt32(12, out var sampleRate))
 582        {
 0583            item.SampleRate = sampleRate;
 584        }
 585
 0586        item.IsDefault = reader.GetBoolean(13);
 0587        item.IsForced = reader.GetBoolean(14);
 0588        item.IsExternal = reader.GetBoolean(15);
 589
 0590        if (reader.TryGetInt32(16, out var width))
 591        {
 0592            item.Width = width;
 593        }
 594
 0595        if (reader.TryGetInt32(17, out var height))
 596        {
 0597            item.Height = height;
 598        }
 599
 0600        if (reader.TryGetSingle(18, out var averageFrameRate))
 601        {
 0602            item.AverageFrameRate = averageFrameRate;
 603        }
 604
 0605        if (reader.TryGetSingle(19, out var realFrameRate))
 606        {
 0607            item.RealFrameRate = realFrameRate;
 608        }
 609
 0610        if (reader.TryGetSingle(20, out var level))
 611        {
 0612            item.Level = level;
 613        }
 614
 0615        if (reader.TryGetString(21, out var pixelFormat))
 616        {
 0617            item.PixelFormat = pixelFormat;
 618        }
 619
 0620        if (reader.TryGetInt32(22, out var bitDepth))
 621        {
 0622            item.BitDepth = bitDepth;
 623        }
 624
 0625        if (reader.TryGetBoolean(23, out var isAnamorphic))
 626        {
 0627            item.IsAnamorphic = isAnamorphic;
 628        }
 629
 0630        if (reader.TryGetInt32(24, out var refFrames))
 631        {
 0632            item.RefFrames = refFrames;
 633        }
 634
 0635        if (reader.TryGetString(25, out var codecTag))
 636        {
 0637            item.CodecTag = codecTag;
 638        }
 639
 0640        if (reader.TryGetString(26, out var comment))
 641        {
 0642            item.Comment = comment;
 643        }
 644
 0645        if (reader.TryGetString(27, out var nalLengthSize))
 646        {
 0647            item.NalLengthSize = nalLengthSize;
 648        }
 649
 0650        if (reader.TryGetBoolean(28, out var isAVC))
 651        {
 0652            item.IsAvc = isAVC;
 653        }
 654
 0655        if (reader.TryGetString(29, out var title))
 656        {
 0657            item.Title = title;
 658        }
 659
 0660        if (reader.TryGetString(30, out var timeBase))
 661        {
 0662            item.TimeBase = timeBase;
 663        }
 664
 0665        if (reader.TryGetString(31, out var codecTimeBase))
 666        {
 0667            item.CodecTimeBase = codecTimeBase;
 668        }
 669
 0670        if (reader.TryGetString(32, out var colorPrimaries))
 671        {
 0672            item.ColorPrimaries = colorPrimaries;
 673        }
 674
 0675        if (reader.TryGetString(33, out var colorSpace))
 676        {
 0677            item.ColorSpace = colorSpace;
 678        }
 679
 0680        if (reader.TryGetString(34, out var colorTransfer))
 681        {
 0682            item.ColorTransfer = colorTransfer;
 683        }
 684
 0685        if (reader.TryGetInt32(35, out var dvVersionMajor))
 686        {
 0687            item.DvVersionMajor = dvVersionMajor;
 688        }
 689
 0690        if (reader.TryGetInt32(36, out var dvVersionMinor))
 691        {
 0692            item.DvVersionMinor = dvVersionMinor;
 693        }
 694
 0695        if (reader.TryGetInt32(37, out var dvProfile))
 696        {
 0697            item.DvProfile = dvProfile;
 698        }
 699
 0700        if (reader.TryGetInt32(38, out var dvLevel))
 701        {
 0702            item.DvLevel = dvLevel;
 703        }
 704
 0705        if (reader.TryGetInt32(39, out var rpuPresentFlag))
 706        {
 0707            item.RpuPresentFlag = rpuPresentFlag;
 708        }
 709
 0710        if (reader.TryGetInt32(40, out var elPresentFlag))
 711        {
 0712            item.ElPresentFlag = elPresentFlag;
 713        }
 714
 0715        if (reader.TryGetInt32(41, out var blPresentFlag))
 716        {
 0717            item.BlPresentFlag = blPresentFlag;
 718        }
 719
 0720        if (reader.TryGetInt32(42, out var dvBlSignalCompatibilityId))
 721        {
 0722            item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId;
 723        }
 724
 0725        item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result;
 726
 727        // if (reader.TryGetInt32(44, out var rotation))
 728        // {
 729        //     item.Rotation = rotation;
 730        // }
 731
 0732        return item;
 733    }
 734
 735    /// <summary>
 736    /// Gets the attachment.
 737    /// </summary>
 738    /// <param name="reader">The reader.</param>
 739    /// <returns>MediaAttachment.</returns>
 740    private AttachmentStreamInfo GetMediaAttachment(SqliteDataReader reader)
 741    {
 0742        var item = new AttachmentStreamInfo
 0743        {
 0744            Index = reader.GetInt32(1),
 0745            Item = null!,
 0746            ItemId = reader.GetGuid(0),
 0747        };
 748
 0749        if (reader.TryGetString(2, out var codec))
 750        {
 0751            item.Codec = codec;
 752        }
 753
 0754        if (reader.TryGetString(3, out var codecTag))
 755        {
 0756            item.CodecTag = codecTag;
 757        }
 758
 0759        if (reader.TryGetString(4, out var comment))
 760        {
 0761            item.Comment = comment;
 762        }
 763
 0764        if (reader.TryGetString(5, out var fileName))
 765        {
 0766            item.Filename = fileName;
 767        }
 768
 0769        if (reader.TryGetString(6, out var mimeType))
 770        {
 0771            item.MimeType = mimeType;
 772        }
 773
 0774        return item;
 775    }
 776
 777    private (BaseItemEntity BaseItem, string[] LegacyUserDataKey) GetItem(SqliteDataReader reader)
 778    {
 0779        var entity = new BaseItemEntity()
 0780        {
 0781            Id = reader.GetGuid(0),
 0782            Type = reader.GetString(1),
 0783        };
 784
 0785        var index = 2;
 786
 0787        if (reader.TryGetString(index++, out var data))
 788        {
 0789            entity.Data = data;
 790        }
 791
 0792        if (reader.TryReadDateTime(index++, out var startDate))
 793        {
 0794            entity.StartDate = startDate;
 795        }
 796
 0797        if (reader.TryReadDateTime(index++, out var endDate))
 798        {
 0799            entity.EndDate = endDate;
 800        }
 801
 0802        if (reader.TryGetGuid(index++, out var guid))
 803        {
 0804            entity.ChannelId = guid;
 805        }
 806
 0807        if (reader.TryGetBoolean(index++, out var isMovie))
 808        {
 0809            entity.IsMovie = isMovie;
 810        }
 811
 0812        if (reader.TryGetBoolean(index++, out var isSeries))
 813        {
 0814            entity.IsSeries = isSeries;
 815        }
 816
 0817        if (reader.TryGetString(index++, out var episodeTitle))
 818        {
 0819            entity.EpisodeTitle = episodeTitle;
 820        }
 821
 0822        if (reader.TryGetBoolean(index++, out var isRepeat))
 823        {
 0824            entity.IsRepeat = isRepeat;
 825        }
 826
 0827        if (reader.TryGetSingle(index++, out var communityRating))
 828        {
 0829            entity.CommunityRating = communityRating;
 830        }
 831
 0832        if (reader.TryGetString(index++, out var customRating))
 833        {
 0834            entity.CustomRating = customRating;
 835        }
 836
 0837        if (reader.TryGetInt32(index++, out var indexNumber))
 838        {
 0839            entity.IndexNumber = indexNumber;
 840        }
 841
 0842        if (reader.TryGetBoolean(index++, out var isLocked))
 843        {
 0844            entity.IsLocked = isLocked;
 845        }
 846
 0847        if (reader.TryGetString(index++, out var preferredMetadataLanguage))
 848        {
 0849            entity.PreferredMetadataLanguage = preferredMetadataLanguage;
 850        }
 851
 0852        if (reader.TryGetString(index++, out var preferredMetadataCountryCode))
 853        {
 0854            entity.PreferredMetadataCountryCode = preferredMetadataCountryCode;
 855        }
 856
 0857        if (reader.TryGetInt32(index++, out var width))
 858        {
 0859            entity.Width = width;
 860        }
 861
 0862        if (reader.TryGetInt32(index++, out var height))
 863        {
 0864            entity.Height = height;
 865        }
 866
 0867        if (reader.TryReadDateTime(index++, out var dateLastRefreshed))
 868        {
 0869            entity.DateLastRefreshed = dateLastRefreshed;
 870        }
 871
 0872        if (reader.TryGetString(index++, out var name))
 873        {
 0874            entity.Name = name;
 875        }
 876
 0877        if (reader.TryGetString(index++, out var restorePath))
 878        {
 0879            entity.Path = restorePath;
 880        }
 881
 0882        if (reader.TryReadDateTime(index++, out var premiereDate))
 883        {
 0884            entity.PremiereDate = premiereDate;
 885        }
 886
 0887        if (reader.TryGetString(index++, out var overview))
 888        {
 0889            entity.Overview = overview;
 890        }
 891
 0892        if (reader.TryGetInt32(index++, out var parentIndexNumber))
 893        {
 0894            entity.ParentIndexNumber = parentIndexNumber;
 895        }
 896
 0897        if (reader.TryGetInt32(index++, out var productionYear))
 898        {
 0899            entity.ProductionYear = productionYear;
 900        }
 901
 0902        if (reader.TryGetString(index++, out var officialRating))
 903        {
 0904            entity.OfficialRating = officialRating;
 905        }
 906
 0907        if (reader.TryGetString(index++, out var forcedSortName))
 908        {
 0909            entity.ForcedSortName = forcedSortName;
 910        }
 911
 0912        if (reader.TryGetInt64(index++, out var runTimeTicks))
 913        {
 0914            entity.RunTimeTicks = runTimeTicks;
 915        }
 916
 0917        if (reader.TryGetInt64(index++, out var size))
 918        {
 0919            entity.Size = size;
 920        }
 921
 0922        if (reader.TryReadDateTime(index++, out var dateCreated))
 923        {
 0924            entity.DateCreated = dateCreated;
 925        }
 926
 0927        if (reader.TryReadDateTime(index++, out var dateModified))
 928        {
 0929            entity.DateModified = dateModified;
 930        }
 931
 0932        if (reader.TryGetString(index++, out var genres))
 933        {
 0934            entity.Genres = genres;
 935        }
 936
 0937        if (reader.TryGetGuid(index++, out var parentId))
 938        {
 0939            entity.ParentId = parentId;
 940        }
 941
 0942        if (reader.TryGetGuid(index++, out var topParentId))
 943        {
 0944            entity.TopParentId = topParentId;
 945        }
 946
 0947        if (reader.TryGetString(index++, out var audioString) && Enum.TryParse<ProgramAudioEntity>(audioString, out var 
 948        {
 0949            entity.Audio = audioType;
 950        }
 951
 0952        if (reader.TryGetString(index++, out var serviceName))
 953        {
 0954            entity.ExternalServiceId = serviceName;
 955        }
 956
 0957        if (reader.TryGetBoolean(index++, out var isInMixedFolder))
 958        {
 0959            entity.IsInMixedFolder = isInMixedFolder;
 960        }
 961
 0962        if (reader.TryReadDateTime(index++, out var dateLastSaved))
 963        {
 0964            entity.DateLastSaved = dateLastSaved;
 965        }
 966
 0967        if (reader.TryGetString(index++, out var lockedFields))
 968        {
 0969            entity.LockedFields = lockedFields.Split('|').Select(Enum.Parse<MetadataField>)
 0970                .Select(e => new BaseItemMetadataField()
 0971                {
 0972                    Id = (int)e,
 0973                    Item = entity,
 0974                    ItemId = entity.Id
 0975                })
 0976                .ToArray();
 977        }
 978
 0979        if (reader.TryGetString(index++, out var studios))
 980        {
 0981            entity.Studios = studios;
 982        }
 983
 0984        if (reader.TryGetString(index++, out var tags))
 985        {
 0986            entity.Tags = tags;
 987        }
 988
 0989        if (reader.TryGetString(index++, out var trailerTypes))
 990        {
 0991            entity.TrailerTypes = trailerTypes.Split('|').Select(Enum.Parse<TrailerType>)
 0992                .Select(e => new BaseItemTrailerType()
 0993                {
 0994                    Id = (int)e,
 0995                    Item = entity,
 0996                    ItemId = entity.Id
 0997                })
 0998                .ToArray();
 999        }
 1000
 01001        if (reader.TryGetString(index++, out var originalTitle))
 1002        {
 01003            entity.OriginalTitle = originalTitle;
 1004        }
 1005
 01006        if (reader.TryGetString(index++, out var primaryVersionId))
 1007        {
 01008            entity.PrimaryVersionId = primaryVersionId;
 1009        }
 1010
 01011        if (reader.TryReadDateTime(index++, out var dateLastMediaAdded))
 1012        {
 01013            entity.DateLastMediaAdded = dateLastMediaAdded;
 1014        }
 1015
 01016        if (reader.TryGetString(index++, out var album))
 1017        {
 01018            entity.Album = album;
 1019        }
 1020
 01021        if (reader.TryGetSingle(index++, out var lUFS))
 1022        {
 01023            entity.LUFS = lUFS;
 1024        }
 1025
 01026        if (reader.TryGetSingle(index++, out var normalizationGain))
 1027        {
 01028            entity.NormalizationGain = normalizationGain;
 1029        }
 1030
 01031        if (reader.TryGetSingle(index++, out var criticRating))
 1032        {
 01033            entity.CriticRating = criticRating;
 1034        }
 1035
 01036        if (reader.TryGetBoolean(index++, out var isVirtualItem))
 1037        {
 01038            entity.IsVirtualItem = isVirtualItem;
 1039        }
 1040
 01041        if (reader.TryGetString(index++, out var seriesName))
 1042        {
 01043            entity.SeriesName = seriesName;
 1044        }
 1045
 01046        var userDataKeys = new List<string>();
 01047        if (reader.TryGetString(index++, out var directUserDataKey))
 1048        {
 01049            userDataKeys.Add(directUserDataKey);
 1050        }
 1051
 01052        if (reader.TryGetString(index++, out var seasonName))
 1053        {
 01054            entity.SeasonName = seasonName;
 1055        }
 1056
 01057        if (reader.TryGetGuid(index++, out var seasonId))
 1058        {
 01059            entity.SeasonId = seasonId;
 1060        }
 1061
 01062        if (reader.TryGetGuid(index++, out var seriesId))
 1063        {
 01064            entity.SeriesId = seriesId;
 1065        }
 1066
 01067        if (reader.TryGetString(index++, out var presentationUniqueKey))
 1068        {
 01069            entity.PresentationUniqueKey = presentationUniqueKey;
 1070        }
 1071
 01072        if (reader.TryGetInt32(index++, out var parentalRating))
 1073        {
 01074            entity.InheritedParentalRatingValue = parentalRating;
 1075        }
 1076
 01077        if (reader.TryGetString(index++, out var externalSeriesId))
 1078        {
 01079            entity.ExternalSeriesId = externalSeriesId;
 1080        }
 1081
 01082        if (reader.TryGetString(index++, out var tagLine))
 1083        {
 01084            entity.Tagline = tagLine;
 1085        }
 1086
 01087        if (reader.TryGetString(index++, out var providerIds))
 1088        {
 01089            entity.Provider = providerIds.Split('|').Select(e => e.Split("="))
 01090            .Select(e => new BaseItemProvider()
 01091            {
 01092                Item = null!,
 01093                ProviderId = e[0],
 01094                ProviderValue = e[1]
 01095            }).ToArray();
 1096        }
 1097
 01098        if (reader.TryGetString(index++, out var imageInfos))
 1099        {
 01100            entity.Images = DeserializeImages(imageInfos).Select(f => Map(entity.Id, f)).ToArray();
 1101        }
 1102
 01103        if (reader.TryGetString(index++, out var productionLocations))
 1104        {
 01105            entity.ProductionLocations = productionLocations;
 1106        }
 1107
 01108        if (reader.TryGetString(index++, out var extraIds))
 1109        {
 01110            entity.ExtraIds = extraIds;
 1111        }
 1112
 01113        if (reader.TryGetInt32(index++, out var totalBitrate))
 1114        {
 01115            entity.TotalBitrate = totalBitrate;
 1116        }
 1117
 01118        if (reader.TryGetString(index++, out var extraTypeString) && Enum.TryParse<BaseItemExtraType>(extraTypeString, o
 1119        {
 01120            entity.ExtraType = extraType;
 1121        }
 1122
 01123        if (reader.TryGetString(index++, out var artists))
 1124        {
 01125            entity.Artists = artists;
 1126        }
 1127
 01128        if (reader.TryGetString(index++, out var albumArtists))
 1129        {
 01130            entity.AlbumArtists = albumArtists;
 1131        }
 1132
 01133        if (reader.TryGetString(index++, out var externalId))
 1134        {
 01135            entity.ExternalId = externalId;
 1136        }
 1137
 01138        if (reader.TryGetString(index++, out var seriesPresentationUniqueKey))
 1139        {
 01140            entity.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
 1141        }
 1142
 01143        if (reader.TryGetString(index++, out var showId))
 1144        {
 01145            entity.ShowId = showId;
 1146        }
 1147
 01148        if (reader.TryGetString(index++, out var ownerId))
 1149        {
 01150            entity.OwnerId = ownerId;
 1151        }
 1152
 01153        if (reader.TryGetString(index++, out var mediaType))
 1154        {
 01155            entity.MediaType = mediaType;
 1156        }
 1157
 01158        if (reader.TryGetString(index++, out var sortName))
 1159        {
 01160            entity.SortName = sortName;
 1161        }
 1162
 01163        if (reader.TryGetString(index++, out var cleanName))
 1164        {
 01165            entity.CleanName = cleanName;
 1166        }
 1167
 01168        if (reader.TryGetString(index++, out var unratedType))
 1169        {
 01170            entity.UnratedType = unratedType;
 1171        }
 1172
 01173        if (reader.TryGetBoolean(index++, out var isFolder))
 1174        {
 01175            entity.IsFolder = isFolder;
 1176        }
 1177
 01178        var baseItem = BaseItemRepository.DeserializeBaseItem(entity, _logger, null, false);
 01179        var dataKeys = baseItem.GetUserDataKeys();
 01180        userDataKeys.AddRange(dataKeys);
 1181
 01182        return (entity, userDataKeys.ToArray());
 1183    }
 1184
 1185    private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
 1186    {
 01187        return new BaseItemImageInfo()
 01188        {
 01189            ItemId = baseItemId,
 01190            Id = Guid.NewGuid(),
 01191            Path = e.Path,
 01192            Blurhash = e.BlurHash != null ? Encoding.UTF8.GetBytes(e.BlurHash) : null,
 01193            DateModified = e.DateModified,
 01194            Height = e.Height,
 01195            Width = e.Width,
 01196            ImageType = (ImageInfoImageType)e.Type,
 01197            Item = null!
 01198        };
 1199    }
 1200
 1201    internal ItemImageInfo[] DeserializeImages(string value)
 1202    {
 01203        if (string.IsNullOrWhiteSpace(value))
 1204        {
 01205            return Array.Empty<ItemImageInfo>();
 1206        }
 1207
 1208        // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the data
 01209        var valueSpan = value.AsSpan();
 01210        var count = valueSpan.Count('|') + 1;
 1211
 01212        var position = 0;
 01213        var result = new ItemImageInfo[count];
 01214        foreach (var part in valueSpan.Split('|'))
 1215        {
 01216            var image = ItemImageInfoFromValueString(part);
 1217
 01218            if (image is not null)
 1219            {
 01220                result[position++] = image;
 1221            }
 1222        }
 1223
 01224        if (position == count)
 1225        {
 01226            return result;
 1227        }
 1228
 01229        if (position == 0)
 1230        {
 01231            return Array.Empty<ItemImageInfo>();
 1232        }
 1233
 1234        // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array.
 01235        return result[..position];
 1236    }
 1237
 1238    internal ItemImageInfo? ItemImageInfoFromValueString(ReadOnlySpan<char> value)
 1239    {
 1240        const char Delimiter = '*';
 1241
 01242        var nextSegment = value.IndexOf(Delimiter);
 01243        if (nextSegment == -1)
 1244        {
 01245            return null;
 1246        }
 1247
 01248        ReadOnlySpan<char> path = value[..nextSegment];
 01249        value = value[(nextSegment + 1)..];
 01250        nextSegment = value.IndexOf(Delimiter);
 01251        if (nextSegment == -1)
 1252        {
 01253            return null;
 1254        }
 1255
 01256        ReadOnlySpan<char> dateModified = value[..nextSegment];
 01257        value = value[(nextSegment + 1)..];
 01258        nextSegment = value.IndexOf(Delimiter);
 01259        if (nextSegment == -1)
 1260        {
 01261            nextSegment = value.Length;
 1262        }
 1263
 01264        ReadOnlySpan<char> imageType = value[..nextSegment];
 1265
 01266        var image = new ItemImageInfo
 01267        {
 01268            Path = path.ToString()
 01269        };
 1270
 01271        if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks)
 01272            && ticks >= DateTime.MinValue.Ticks
 01273            && ticks <= DateTime.MaxValue.Ticks)
 1274        {
 01275            image.DateModified = new DateTime(ticks, DateTimeKind.Utc);
 1276        }
 1277        else
 1278        {
 01279            return null;
 1280        }
 1281
 01282        if (Enum.TryParse(imageType, true, out ImageType type))
 1283        {
 01284            image.Type = type;
 1285        }
 1286        else
 1287        {
 01288            return null;
 1289        }
 1290
 1291        // Optional parameters: width*height*blurhash
 01292        if (nextSegment + 1 < value.Length - 1)
 1293        {
 01294            value = value[(nextSegment + 1)..];
 01295            nextSegment = value.IndexOf(Delimiter);
 01296            if (nextSegment == -1 || nextSegment == value.Length)
 1297            {
 01298                return image;
 1299            }
 1300
 01301            ReadOnlySpan<char> widthSpan = value[..nextSegment];
 1302
 01303            value = value[(nextSegment + 1)..];
 01304            nextSegment = value.IndexOf(Delimiter);
 01305            if (nextSegment == -1)
 1306            {
 01307                nextSegment = value.Length;
 1308            }
 1309
 01310            ReadOnlySpan<char> heightSpan = value[..nextSegment];
 1311
 01312            if (int.TryParse(widthSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var width)
 01313                && int.TryParse(heightSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var height))
 1314            {
 01315                image.Width = width;
 01316                image.Height = height;
 1317            }
 1318
 01319            if (nextSegment < value.Length - 1)
 1320            {
 01321                value = value[(nextSegment + 1)..];
 01322                var length = value.Length;
 1323
 01324                Span<char> blurHashSpan = stackalloc char[length];
 01325                for (int i = 0; i < length; i++)
 1326                {
 01327                    var c = value[i];
 01328                    blurHashSpan[i] = c switch
 01329                    {
 01330                        '/' => Delimiter,
 01331                        '\\' => '|',
 01332                        _ => c
 01333                    };
 1334                }
 1335
 01336                image.BlurHash = new string(blurHashSpan);
 1337            }
 1338        }
 1339
 01340        return image;
 1341    }
 1342
 1343    private class TrackedMigrationStep : IDisposable
 1344    {
 1345        private readonly string _operationName;
 1346        private readonly ILogger _logger;
 1347        private readonly Stopwatch _operationTimer;
 1348        private bool _disposed;
 1349
 1350        public TrackedMigrationStep(string operationName, ILogger logger)
 1351        {
 01352            _operationName = operationName;
 01353            _logger = logger;
 01354            _operationTimer = Stopwatch.StartNew();
 01355            logger.LogInformation("Start {OperationName}", operationName);
 01356        }
 1357
 1358        public bool Disposed
 1359        {
 01360            get => _disposed;
 01361            set => _disposed = value;
 1362        }
 1363
 1364        public virtual void Dispose()
 1365        {
 01366            if (Disposed)
 1367            {
 01368                return;
 1369            }
 1370
 01371            Disposed = true;
 01372            _logger.LogInformation("{OperationName} took '{Time}'", _operationName, _operationTimer.Elapsed);
 01373        }
 1374    }
 1375
 1376    private sealed class DatabaseMigrationStep : TrackedMigrationStep
 1377    {
 01378        public DatabaseMigrationStep(JellyfinDbContext jellyfinDbContext, string operationName, ILogger logger) : base(o
 1379        {
 1380            JellyfinDbContext = jellyfinDbContext;
 01381        }
 1382
 1383        public JellyfinDbContext JellyfinDbContext { get; }
 1384
 1385        public override void Dispose()
 1386        {
 01387            if (Disposed)
 1388            {
 01389                return;
 1390            }
 1391
 01392            JellyfinDbContext.Dispose();
 01393            base.Dispose();
 01394        }
 1395    }
 1396}