< 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: 642
Coverable lines: 642
Total lines: 1482
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 364
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 11/28/2025 - 12:11:11 AM Line coverage: 0% (0/633) Branch coverage: 0% (0/362) Total lines: 14671/10/2026 - 12:12:36 AM Line coverage: 0% (0/634) Branch coverage: 0% (0/364) Total lines: 14702/15/2026 - 12:13:43 AM Line coverage: 0% (0/636) Branch coverage: 0% (0/364) Total lines: 14723/2/2026 - 12:14:15 AM Line coverage: 0% (0/642) Branch coverage: 0% (0/364) Total lines: 1482 11/28/2025 - 12:11:11 AM Line coverage: 0% (0/633) Branch coverage: 0% (0/362) Total lines: 14671/10/2026 - 12:12:36 AM Line coverage: 0% (0/634) Branch coverage: 0% (0/364) Total lines: 14702/15/2026 - 12:13:43 AM Line coverage: 0% (0/636) Branch coverage: 0% (0/364) Total lines: 14723/2/2026 - 12:14:15 AM Line coverage: 0% (0/642) Branch coverage: 0% (0/364) Total lines: 1482

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
Perform()0%3422580%
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%238701540%
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        {
 0102            IDictionary<Guid, (BaseItemEntity BaseItem, string[] Keys)> allItemsLookup = new Dictionary<Guid, (BaseItemE
 103            const string typedBaseItemsQuery =
 104            """
 105            SELECT guid, type, data, StartDate, EndDate, ChannelId, IsMovie,
 106            IsSeries, EpisodeTitle, IsRepeat, CommunityRating, CustomRating, IndexNumber, IsLocked, PreferredMetadataLan
 107            PreferredMetadataCountryCode, Width, Height, DateLastRefreshed, Name, Path, PremiereDate, Overview, ParentIn
 108            ProductionYear, OfficialRating, ForcedSortName, RunTimeTicks, Size, DateCreated, DateModified, Genres, Paren
 109            Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, Origina
 110            DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, Se
 111            PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, Product
 112            ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortN
 113            """;
 0114            using (new TrackedMigrationStep("Loading TypedBaseItems", _logger))
 115            {
 0116                foreach (SqliteDataReader dto in connection.Query(typedBaseItemsQuery))
 117                {
 0118                    var baseItem = GetItem(dto);
 0119                    allItemsLookup.Add(baseItem.BaseItem.Id, baseItem);
 120                }
 121            }
 122
 123            bool DoesResolve(Guid? parentId, HashSet<(BaseItemEntity BaseItem, string[] Keys)> checkStack)
 124            {
 125                if (parentId is null)
 126                {
 127                    return true;
 128                }
 129
 130                if (!allItemsLookup.TryGetValue(parentId.Value, out var parent))
 131                {
 132                    return false; // item is detached and has no root anymore.
 133                }
 134
 135                if (!checkStack.Add(parent))
 136                {
 137                    return false; // recursive structure. Abort.
 138                }
 139
 140                return DoesResolve(parent.BaseItem.ParentId, checkStack);
 141            }
 142
 0143            using (new TrackedMigrationStep("Clean TypedBaseItems hierarchy", _logger))
 144            {
 0145                var checkStack = new HashSet<(BaseItemEntity BaseItem, string[] Keys)>();
 146
 0147                foreach (var item in allItemsLookup)
 148                {
 0149                    var cachedItem = item.Value;
 0150                    if (DoesResolve(cachedItem.BaseItem.ParentId, checkStack))
 151                    {
 0152                        checkStack.Add(cachedItem);
 0153                        operation.JellyfinDbContext.BaseItems.Add(cachedItem.BaseItem);
 0154                        baseItemIds.Add(cachedItem.BaseItem.Id);
 0155                        foreach (var dataKey in cachedItem.Keys)
 156                        {
 0157                            legacyBaseItemWithUserKeys[dataKey] = cachedItem.BaseItem;
 158                        }
 159                    }
 160
 0161                    checkStack.Clear();
 162                }
 163            }
 164
 0165            using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.BaseItems.Local.Count} BaseItem entrie
 166            {
 0167                operation.JellyfinDbContext.SaveChanges();
 0168            }
 169
 0170            allItemsLookup.Clear();
 0171        }
 172
 0173        using (var operation = GetPreparedDbContext("Moving ItemValues"))
 174        {
 175            // do not migrate inherited types as they are now properly mapped in search and lookup.
 176            const string itemValueQuery =
 177            """
 178            SELECT ItemId, Type, Value, CleanValue FROM ItemValues
 179                        WHERE Type <> 6 AND EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = ItemValues.I
 180            """;
 181
 182            // EFCores local lookup sucks. We cannot use context.ItemValues.Local here because its just super slow.
 0183            var localItems = new Dictionary<(int Type, string Value), (Database.Implementations.Entities.ItemValue ItemV
 0184            using (new TrackedMigrationStep("Loading ItemValues", _logger))
 185            {
 0186                foreach (SqliteDataReader dto in connection.Query(itemValueQuery))
 187                {
 0188                    var itemId = dto.GetGuid(0);
 0189                    if (!baseItemIds.Contains(itemId))
 190                    {
 191                        continue;
 192                    }
 193
 0194                    var entity = GetItemValue(dto);
 0195                    var key = ((int)entity.Type, entity.Value);
 0196                    if (!localItems.TryGetValue(key, out var existing))
 197                    {
 0198                        localItems[key] = existing = (entity, []);
 199                    }
 200
 0201                    existing.ItemIds.Add(itemId);
 202                }
 203
 0204                foreach (var item in localItems)
 205                {
 0206                    operation.JellyfinDbContext.ItemValues.Add(item.Value.ItemValue);
 0207                    operation.JellyfinDbContext.ItemValuesMap.AddRange(item.Value.ItemIds.Distinct().Select(f => new Ite
 0208                    {
 0209                        Item = null!,
 0210                        ItemValue = null!,
 0211                        ItemId = f,
 0212                        ItemValueId = item.Value.ItemValue.ItemValueId
 0213                    }));
 214                }
 215            }
 216
 0217            using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.ItemValues.Local.Count} ItemValues ent
 218            {
 0219                operation.JellyfinDbContext.SaveChanges();
 0220            }
 221        }
 222
 0223        using (var operation = GetPreparedDbContext("Moving UserData"))
 224        {
 0225            var queryResult = connection.Query(
 0226            """
 0227            SELECT key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStrea
 0228
 0229            WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.UserDataKey = UserDatas.key)
 0230            """);
 231
 0232            using (new TrackedMigrationStep("Loading UserData", _logger))
 233            {
 0234                var users = operation.JellyfinDbContext.Users.AsNoTracking().ToArray();
 0235                var userIdBlacklist = new HashSet<int>();
 236
 0237                foreach (var entity in queryResult)
 238                {
 0239                    var userData = GetUserData(users, entity, userIdBlacklist, _logger);
 0240                    if (userData is null)
 241                    {
 0242                        var userDataId = entity.GetString(0);
 0243                        var internalUserId = entity.GetInt32(1);
 244
 0245                        if (!userIdBlacklist.Contains(internalUserId))
 246                        {
 0247                            _logger.LogError("Was not able to migrate user data with key {0} because its id {InternalId}
 0248                            userIdBlacklist.Add(internalUserId);
 249                        }
 250
 0251                        continue;
 252                    }
 253
 0254                    if (!legacyBaseItemWithUserKeys.TryGetValue(userData.CustomDataKey!, out var refItem))
 255                    {
 0256                        _logger.LogError("Was not able to migrate user data with key {0} because it does not reference a
 0257                        continue;
 258                    }
 259
 0260                    if (!baseItemIds.Contains(refItem.Id))
 261                    {
 262                        continue;
 263                    }
 264
 0265                    userData.ItemId = refItem.Id;
 0266                    operation.JellyfinDbContext.UserData.Add(userData);
 267                }
 268            }
 269
 0270            legacyBaseItemWithUserKeys.Clear();
 271
 0272            using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.UserData.Local.Count} UserData entries
 273            {
 0274                operation.JellyfinDbContext.SaveChanges();
 0275            }
 276        }
 277
 0278        using (var operation = GetPreparedDbContext("Moving MediaStreamInfos"))
 279        {
 280            const string mediaStreamQuery =
 281            """
 282            SELECT ItemId, StreamIndex, StreamType, Codec, Language, ChannelLayout, Profile, AspectRatio, Path,
 283            IsInterlaced, BitRate, Channels, SampleRate, IsDefault, IsForced, IsExternal, Height, Width,
 284            AverageFrameRate, RealFrameRate, Level, PixelFormat, BitDepth, IsAnamorphic, RefFrames, CodecTag,
 285            Comment, NalLengthSize, IsAvc, Title, TimeBase, CodecTimeBase, ColorPrimaries, ColorSpace, ColorTransfer,
 286            DvVersionMajor, DvVersionMinor, DvProfile, DvLevel, RpuPresentFlag, ElPresentFlag, BlPresentFlag, DvBlSignal
 287            FROM MediaStreams
 288            WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = MediaStreams.ItemId)
 289            """;
 290
 0291            using (new TrackedMigrationStep("Loading MediaStreamInfos", _logger))
 292            {
 0293                foreach (SqliteDataReader dto in connection.Query(mediaStreamQuery))
 294                {
 0295                    var entity = GetMediaStream(dto);
 0296                    if (!baseItemIds.Contains(entity.ItemId))
 297                    {
 298                        continue;
 299                    }
 300
 0301                    operation.JellyfinDbContext.MediaStreamInfos.Add(entity);
 302                }
 303            }
 304
 0305            using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.MediaStreamInfos.Local.Count} MediaStr
 306            {
 0307                operation.JellyfinDbContext.SaveChanges();
 0308            }
 309        }
 310
 0311        using (var operation = GetPreparedDbContext("Moving AttachmentStreamInfos"))
 312        {
 313            const string mediaAttachmentQuery =
 314            """
 315            SELECT ItemId, AttachmentIndex, Codec, CodecTag, Comment, filename, MIMEType
 316            FROM mediaattachments
 317            WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = mediaattachments.ItemId)
 318            """;
 319
 0320            using (new TrackedMigrationStep("Loading AttachmentStreamInfos", _logger))
 321            {
 0322                foreach (SqliteDataReader dto in connection.Query(mediaAttachmentQuery))
 323                {
 0324                    var entity = GetMediaAttachment(dto);
 0325                    if (!baseItemIds.Contains(entity.ItemId))
 326                    {
 327                        continue;
 328                    }
 329
 0330                    operation.JellyfinDbContext.AttachmentStreamInfos.Add(entity);
 331                }
 332            }
 333
 0334            using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.AttachmentStreamInfos.Local.Count} Att
 335            {
 0336                operation.JellyfinDbContext.SaveChanges();
 0337            }
 338        }
 339
 0340        using (var operation = GetPreparedDbContext("Moving People"))
 341        {
 342            const string personsQuery =
 343            """
 344            SELECT ItemId, Name, Role, PersonType, SortOrder, ListOrder FROM People
 345            WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = People.ItemId)
 346            """;
 347
 0348            var peopleCache = new Dictionary<string, (People Person, List<PeopleBaseItemMap> Items)>();
 349
 0350            using (new TrackedMigrationStep("Loading People", _logger))
 351            {
 0352                foreach (SqliteDataReader reader in connection.Query(personsQuery))
 353                {
 0354                    var itemId = reader.GetGuid(0);
 0355                    if (!baseItemIds.Contains(itemId))
 356                    {
 0357                        _logger.LogError("Not saving person {0} because it's not in use by any BaseItem", reader.GetStri
 0358                        continue;
 359                    }
 360
 0361                    var entity = GetPerson(reader);
 0362                    if (!peopleCache.TryGetValue(entity.Name + "|" + entity.PersonType, out var personCache))
 363                    {
 0364                        peopleCache[entity.Name + "|" + entity.PersonType] = personCache = (entity, []);
 365                    }
 366
 0367                    if (reader.TryGetString(2, out var role))
 368                    {
 369                    }
 370
 0371                    int? sortOrder = reader.IsDBNull(4) ? null : reader.GetInt32(4);
 0372                    int? listOrder = reader.IsDBNull(5) ? null : reader.GetInt32(5);
 373
 0374                    personCache.Items.Add(new PeopleBaseItemMap()
 0375                    {
 0376                        Item = null!,
 0377                        ItemId = itemId,
 0378                        People = null!,
 0379                        PeopleId = personCache.Person.Id,
 0380                        ListOrder = listOrder,
 0381                        SortOrder = sortOrder,
 0382                        Role = role
 0383                    });
 384                }
 385
 0386                foreach (var item in peopleCache)
 387                {
 0388                    operation.JellyfinDbContext.Peoples.Add(item.Value.Person);
 0389                    operation.JellyfinDbContext.PeopleBaseItemMap.AddRange(item.Value.Items.DistinctBy(e => (e.ItemId, e
 390                }
 391
 0392                peopleCache.Clear();
 0393            }
 394
 0395            using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.Peoples.Local.Count} People entries an
 396            {
 0397                operation.JellyfinDbContext.SaveChanges();
 0398            }
 399        }
 400
 0401        using (var operation = GetPreparedDbContext("Moving Chapters"))
 402        {
 403            const string chapterQuery =
 404            """
 405            SELECT ItemId,StartPositionTicks,Name,ImagePath,ImageDateModified,ChapterIndex from Chapters2
 406            WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = Chapters2.ItemId)
 407            """;
 408
 0409            using (new TrackedMigrationStep("Loading Chapters", _logger))
 410            {
 0411                foreach (SqliteDataReader dto in connection.Query(chapterQuery))
 412                {
 0413                    var chapter = GetChapter(dto);
 0414                    if (!baseItemIds.Contains(chapter.ItemId))
 415                    {
 416                        continue;
 417                    }
 418
 0419                    operation.JellyfinDbContext.Chapters.Add(chapter);
 420                }
 421            }
 422
 0423            using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.Chapters.Local.Count} Chapters entries
 424            {
 0425                operation.JellyfinDbContext.SaveChanges();
 0426            }
 427        }
 428
 0429        using (var operation = GetPreparedDbContext("Moving AncestorIds"))
 430        {
 431            const string ancestorIdsQuery =
 432            """
 433            SELECT ItemId, AncestorId, AncestorIdText FROM AncestorIds
 434            WHERE
 435            EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.ItemId)
 436            AND
 437            EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.AncestorId)
 438            """;
 439
 0440            using (new TrackedMigrationStep("Loading AncestorIds", _logger))
 441            {
 0442                foreach (SqliteDataReader dto in connection.Query(ancestorIdsQuery))
 443                {
 0444                    var ancestorId = GetAncestorId(dto);
 0445                    if (!baseItemIds.Contains(ancestorId.ItemId) || !baseItemIds.Contains(ancestorId.ParentItemId))
 446                    {
 447                        continue;
 448                    }
 449
 0450                    operation.JellyfinDbContext.AncestorIds.Add(ancestorId);
 451                }
 452            }
 453
 0454            using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.AncestorIds.Local.Count} AncestorId en
 455            {
 0456                operation.JellyfinDbContext.SaveChanges();
 0457            }
 458        }
 459
 0460        connection.Close();
 461
 0462        _logger.LogInformation("Migration of the Library.db done.");
 0463        _logger.LogInformation("Migrating Library db took {0}.", fullOperationTimer.Elapsed);
 464
 0465        SqliteConnection.ClearAllPools();
 466
 0467        using (var checkpointConnection = new SqliteConnection($"Filename={libraryDbPath}"))
 468        {
 0469            checkpointConnection.Open();
 0470            using var cmd = checkpointConnection.CreateCommand();
 0471            cmd.CommandText = "PRAGMA wal_checkpoint(TRUNCATE);";
 0472            cmd.ExecuteNonQuery();
 473        }
 474
 0475        SqliteConnection.ClearAllPools();
 476
 0477        _logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old");
 0478        File.Move(libraryDbPath, libraryDbPath + ".old", true);
 0479    }
 480
 481    private DatabaseMigrationStep GetPreparedDbContext(string operationName)
 482    {
 0483        var dbContext = _provider.CreateDbContext();
 0484        dbContext.ChangeTracker.AutoDetectChangesEnabled = false;
 0485        dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
 0486        return new DatabaseMigrationStep(dbContext, operationName, _logger);
 487    }
 488
 489    internal static UserData? GetUserData(User[] users, SqliteDataReader dto, HashSet<int> userIdBlacklist, ILogger logg
 490    {
 0491        var internalUserId = dto.GetInt32(1);
 0492        if (userIdBlacklist.Contains(internalUserId))
 493        {
 0494            return null;
 495        }
 496
 0497        var user = users.FirstOrDefault(e => e.InternalId == internalUserId);
 0498        if (user is null)
 499        {
 0500            userIdBlacklist.Add(internalUserId);
 501
 0502            logger.LogError("Tried to find user with index '{Idx}' but there are only '{MaxIdx}' users.", internalUserId
 0503            return null;
 504        }
 505
 0506        var oldKey = dto.GetString(0);
 507
 0508        return new UserData()
 0509        {
 0510            ItemId = Guid.NewGuid(),
 0511            CustomDataKey = oldKey,
 0512            UserId = user.Id,
 0513            Rating = dto.IsDBNull(2) ? null : dto.GetDouble(2),
 0514            Played = dto.GetBoolean(3),
 0515            PlayCount = dto.GetInt32(4),
 0516            IsFavorite = dto.GetBoolean(5),
 0517            PlaybackPositionTicks = dto.GetInt64(6),
 0518            LastPlayedDate = dto.IsDBNull(7) ? null : dto.GetDateTime(7),
 0519            AudioStreamIndex = dto.IsDBNull(8) ? null : dto.GetInt32(8),
 0520            SubtitleStreamIndex = dto.IsDBNull(9) ? null : dto.GetInt32(9),
 0521            Likes = null,
 0522            User = null!,
 0523            Item = null!
 0524        };
 525    }
 526
 527    private AncestorId GetAncestorId(SqliteDataReader reader)
 528    {
 0529        return new AncestorId()
 0530        {
 0531            ItemId = reader.GetGuid(0),
 0532            ParentItemId = reader.GetGuid(1),
 0533            Item = null!,
 0534            ParentItem = null!
 0535        };
 536    }
 537
 538    /// <summary>
 539    /// Gets the chapter.
 540    /// </summary>
 541    /// <param name="reader">The reader.</param>
 542    /// <returns>ChapterInfo.</returns>
 543    private Chapter GetChapter(SqliteDataReader reader)
 544    {
 0545        var chapter = new Chapter
 0546        {
 0547            StartPositionTicks = reader.GetInt64(1),
 0548            ChapterIndex = reader.GetInt32(5),
 0549            Item = null!,
 0550            ItemId = reader.GetGuid(0),
 0551        };
 552
 0553        if (reader.TryGetString(2, out var chapterName))
 554        {
 0555            chapter.Name = chapterName;
 556        }
 557
 0558        if (reader.TryGetString(3, out var imagePath))
 559        {
 0560            chapter.ImagePath = imagePath;
 561        }
 562
 0563        if (reader.TryReadDateTime(4, out var imageDateModified))
 564        {
 0565            chapter.ImageDateModified = imageDateModified;
 566        }
 567
 0568        return chapter;
 569    }
 570
 571    private ItemValue GetItemValue(SqliteDataReader reader)
 572    {
 0573        return new ItemValue
 0574        {
 0575            ItemValueId = Guid.NewGuid(),
 0576            Type = (ItemValueType)reader.GetInt32(1),
 0577            Value = reader.GetString(2),
 0578            CleanValue = reader.GetString(3),
 0579        };
 580    }
 581
 582    private People GetPerson(SqliteDataReader reader)
 583    {
 0584        var item = new People
 0585        {
 0586            Id = Guid.NewGuid(),
 0587            Name = reader.GetString(1),
 0588        };
 589
 0590        if (reader.TryGetString(3, out var type))
 591        {
 0592            item.PersonType = type;
 593        }
 594
 0595        return item;
 596    }
 597
 598    /// <summary>
 599    /// Gets the media stream.
 600    /// </summary>
 601    /// <param name="reader">The reader.</param>
 602    /// <returns>MediaStream.</returns>
 603    private MediaStreamInfo GetMediaStream(SqliteDataReader reader)
 604    {
 0605        var item = new MediaStreamInfo
 0606        {
 0607            StreamIndex = reader.GetInt32(1),
 0608            StreamType = Enum.Parse<MediaStreamTypeEntity>(reader.GetString(2)),
 0609            Item = null!,
 0610            ItemId = reader.GetGuid(0),
 0611            AspectRatio = null!,
 0612            ChannelLayout = null!,
 0613            Codec = null!,
 0614            IsInterlaced = false,
 0615            Language = null!,
 0616            Path = null!,
 0617            Profile = null!,
 0618        };
 619
 0620        if (reader.TryGetString(3, out var codec))
 621        {
 0622            item.Codec = codec;
 623        }
 624
 0625        if (reader.TryGetString(4, out var language))
 626        {
 0627            item.Language = language;
 628        }
 629
 0630        if (reader.TryGetString(5, out var channelLayout))
 631        {
 0632            item.ChannelLayout = channelLayout;
 633        }
 634
 0635        if (reader.TryGetString(6, out var profile))
 636        {
 0637            item.Profile = profile;
 638        }
 639
 0640        if (reader.TryGetString(7, out var aspectRatio))
 641        {
 0642            item.AspectRatio = aspectRatio;
 643        }
 644
 0645        if (reader.TryGetString(8, out var path))
 646        {
 0647            item.Path = path;
 648        }
 649
 0650        item.IsInterlaced = reader.GetBoolean(9);
 651
 0652        if (reader.TryGetInt32(10, out var bitrate))
 653        {
 0654            item.BitRate = bitrate;
 655        }
 656
 0657        if (reader.TryGetInt32(11, out var channels))
 658        {
 0659            item.Channels = channels;
 660        }
 661
 0662        if (reader.TryGetInt32(12, out var sampleRate))
 663        {
 0664            item.SampleRate = sampleRate;
 665        }
 666
 0667        item.IsDefault = reader.GetBoolean(13);
 0668        item.IsForced = reader.GetBoolean(14);
 0669        item.IsExternal = reader.GetBoolean(15);
 670
 0671        if (reader.TryGetInt32(16, out var width))
 672        {
 0673            item.Width = width;
 674        }
 675
 0676        if (reader.TryGetInt32(17, out var height))
 677        {
 0678            item.Height = height;
 679        }
 680
 0681        if (reader.TryGetSingle(18, out var averageFrameRate))
 682        {
 0683            item.AverageFrameRate = averageFrameRate;
 684        }
 685
 0686        if (reader.TryGetSingle(19, out var realFrameRate))
 687        {
 0688            item.RealFrameRate = realFrameRate;
 689        }
 690
 0691        if (reader.TryGetSingle(20, out var level))
 692        {
 0693            item.Level = level;
 694        }
 695
 0696        if (reader.TryGetString(21, out var pixelFormat))
 697        {
 0698            item.PixelFormat = pixelFormat;
 699        }
 700
 0701        if (reader.TryGetInt32(22, out var bitDepth))
 702        {
 0703            item.BitDepth = bitDepth;
 704        }
 705
 0706        if (reader.TryGetBoolean(23, out var isAnamorphic))
 707        {
 0708            item.IsAnamorphic = isAnamorphic;
 709        }
 710
 0711        if (reader.TryGetInt32(24, out var refFrames))
 712        {
 0713            item.RefFrames = refFrames;
 714        }
 715
 0716        if (reader.TryGetString(25, out var codecTag))
 717        {
 0718            item.CodecTag = codecTag;
 719        }
 720
 0721        if (reader.TryGetString(26, out var comment))
 722        {
 0723            item.Comment = comment;
 724        }
 725
 0726        if (reader.TryGetString(27, out var nalLengthSize))
 727        {
 0728            item.NalLengthSize = nalLengthSize;
 729        }
 730
 0731        if (reader.TryGetBoolean(28, out var isAVC))
 732        {
 0733            item.IsAvc = isAVC;
 734        }
 735
 0736        if (reader.TryGetString(29, out var title))
 737        {
 0738            item.Title = title;
 739        }
 740
 0741        if (reader.TryGetString(30, out var timeBase))
 742        {
 0743            item.TimeBase = timeBase;
 744        }
 745
 0746        if (reader.TryGetString(31, out var codecTimeBase))
 747        {
 0748            item.CodecTimeBase = codecTimeBase;
 749        }
 750
 0751        if (reader.TryGetString(32, out var colorPrimaries))
 752        {
 0753            item.ColorPrimaries = colorPrimaries;
 754        }
 755
 0756        if (reader.TryGetString(33, out var colorSpace))
 757        {
 0758            item.ColorSpace = colorSpace;
 759        }
 760
 0761        if (reader.TryGetString(34, out var colorTransfer))
 762        {
 0763            item.ColorTransfer = colorTransfer;
 764        }
 765
 0766        if (reader.TryGetInt32(35, out var dvVersionMajor))
 767        {
 0768            item.DvVersionMajor = dvVersionMajor;
 769        }
 770
 0771        if (reader.TryGetInt32(36, out var dvVersionMinor))
 772        {
 0773            item.DvVersionMinor = dvVersionMinor;
 774        }
 775
 0776        if (reader.TryGetInt32(37, out var dvProfile))
 777        {
 0778            item.DvProfile = dvProfile;
 779        }
 780
 0781        if (reader.TryGetInt32(38, out var dvLevel))
 782        {
 0783            item.DvLevel = dvLevel;
 784        }
 785
 0786        if (reader.TryGetInt32(39, out var rpuPresentFlag))
 787        {
 0788            item.RpuPresentFlag = rpuPresentFlag;
 789        }
 790
 0791        if (reader.TryGetInt32(40, out var elPresentFlag))
 792        {
 0793            item.ElPresentFlag = elPresentFlag;
 794        }
 795
 0796        if (reader.TryGetInt32(41, out var blPresentFlag))
 797        {
 0798            item.BlPresentFlag = blPresentFlag;
 799        }
 800
 0801        if (reader.TryGetInt32(42, out var dvBlSignalCompatibilityId))
 802        {
 0803            item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId;
 804        }
 805
 0806        item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result;
 807
 808        // if (reader.TryGetInt32(44, out var rotation))
 809        // {
 810        //     item.Rotation = rotation;
 811        // }
 812
 0813        return item;
 814    }
 815
 816    /// <summary>
 817    /// Gets the attachment.
 818    /// </summary>
 819    /// <param name="reader">The reader.</param>
 820    /// <returns>MediaAttachment.</returns>
 821    private AttachmentStreamInfo GetMediaAttachment(SqliteDataReader reader)
 822    {
 0823        var item = new AttachmentStreamInfo
 0824        {
 0825            Index = reader.GetInt32(1),
 0826            Item = null!,
 0827            ItemId = reader.GetGuid(0),
 0828        };
 829
 0830        if (reader.TryGetString(2, out var codec))
 831        {
 0832            item.Codec = codec;
 833        }
 834
 0835        if (reader.TryGetString(3, out var codecTag))
 836        {
 0837            item.CodecTag = codecTag;
 838        }
 839
 0840        if (reader.TryGetString(4, out var comment))
 841        {
 0842            item.Comment = comment;
 843        }
 844
 0845        if (reader.TryGetString(5, out var fileName))
 846        {
 0847            item.Filename = fileName;
 848        }
 849
 0850        if (reader.TryGetString(6, out var mimeType))
 851        {
 0852            item.MimeType = mimeType;
 853        }
 854
 0855        return item;
 856    }
 857
 858    private (BaseItemEntity BaseItem, string[] LegacyUserDataKey) GetItem(SqliteDataReader reader)
 859    {
 0860        var entity = new BaseItemEntity()
 0861        {
 0862            Id = reader.GetGuid(0),
 0863            Type = reader.GetString(1),
 0864        };
 865
 0866        var index = 2;
 867
 0868        if (reader.TryGetString(index++, out var data))
 869        {
 0870            entity.Data = data;
 871        }
 872
 0873        if (reader.TryReadDateTime(index++, out var startDate))
 874        {
 0875            entity.StartDate = startDate;
 876        }
 877
 0878        if (reader.TryReadDateTime(index++, out var endDate))
 879        {
 0880            entity.EndDate = endDate;
 881        }
 882
 0883        if (reader.TryGetGuid(index++, out var guid))
 884        {
 0885            entity.ChannelId = guid;
 886        }
 887
 0888        if (reader.TryGetBoolean(index++, out var isMovie))
 889        {
 0890            entity.IsMovie = isMovie;
 891        }
 892
 0893        if (reader.TryGetBoolean(index++, out var isSeries))
 894        {
 0895            entity.IsSeries = isSeries;
 896        }
 897
 0898        if (reader.TryGetString(index++, out var episodeTitle))
 899        {
 0900            entity.EpisodeTitle = episodeTitle;
 901        }
 902
 0903        if (reader.TryGetBoolean(index++, out var isRepeat))
 904        {
 0905            entity.IsRepeat = isRepeat;
 906        }
 907
 0908        if (reader.TryGetSingle(index++, out var communityRating))
 909        {
 0910            entity.CommunityRating = communityRating;
 911        }
 912
 0913        if (reader.TryGetString(index++, out var customRating))
 914        {
 0915            entity.CustomRating = customRating;
 916        }
 917
 0918        if (reader.TryGetInt32(index++, out var indexNumber))
 919        {
 0920            entity.IndexNumber = indexNumber;
 921        }
 922
 0923        if (reader.TryGetBoolean(index++, out var isLocked))
 924        {
 0925            entity.IsLocked = isLocked;
 926        }
 927
 0928        if (reader.TryGetString(index++, out var preferredMetadataLanguage))
 929        {
 0930            entity.PreferredMetadataLanguage = preferredMetadataLanguage;
 931        }
 932
 0933        if (reader.TryGetString(index++, out var preferredMetadataCountryCode))
 934        {
 0935            entity.PreferredMetadataCountryCode = preferredMetadataCountryCode;
 936        }
 937
 0938        if (reader.TryGetInt32(index++, out var width))
 939        {
 0940            entity.Width = width;
 941        }
 942
 0943        if (reader.TryGetInt32(index++, out var height))
 944        {
 0945            entity.Height = height;
 946        }
 947
 0948        if (reader.TryReadDateTime(index++, out var dateLastRefreshed))
 949        {
 0950            entity.DateLastRefreshed = dateLastRefreshed;
 951        }
 952
 0953        if (reader.TryGetString(index++, out var name))
 954        {
 0955            entity.Name = name;
 956        }
 957
 0958        if (reader.TryGetString(index++, out var restorePath))
 959        {
 0960            entity.Path = restorePath;
 961        }
 962
 0963        if (reader.TryReadDateTime(index++, out var premiereDate))
 964        {
 0965            entity.PremiereDate = premiereDate;
 966        }
 967
 0968        if (reader.TryGetString(index++, out var overview))
 969        {
 0970            entity.Overview = overview;
 971        }
 972
 0973        if (reader.TryGetInt32(index++, out var parentIndexNumber))
 974        {
 0975            entity.ParentIndexNumber = parentIndexNumber;
 976        }
 977
 0978        if (reader.TryGetInt32(index++, out var productionYear))
 979        {
 0980            entity.ProductionYear = productionYear;
 981        }
 982
 0983        if (reader.TryGetString(index++, out var officialRating))
 984        {
 0985            entity.OfficialRating = officialRating;
 986        }
 987
 0988        if (reader.TryGetString(index++, out var forcedSortName))
 989        {
 0990            entity.ForcedSortName = forcedSortName;
 991        }
 992
 0993        if (reader.TryGetInt64(index++, out var runTimeTicks))
 994        {
 0995            entity.RunTimeTicks = runTimeTicks;
 996        }
 997
 0998        if (reader.TryGetInt64(index++, out var size))
 999        {
 01000            entity.Size = size;
 1001        }
 1002
 01003        if (reader.TryReadDateTime(index++, out var dateCreated))
 1004        {
 01005            entity.DateCreated = dateCreated;
 1006        }
 1007
 01008        if (reader.TryReadDateTime(index++, out var dateModified))
 1009        {
 01010            entity.DateModified = dateModified;
 1011        }
 1012
 01013        if (reader.TryGetString(index++, out var genres))
 1014        {
 01015            entity.Genres = genres;
 1016        }
 1017
 01018        if (reader.TryGetGuid(index++, out var parentId))
 1019        {
 01020            entity.ParentId = parentId;
 1021        }
 1022
 01023        if (reader.TryGetGuid(index++, out var topParentId))
 1024        {
 01025            entity.TopParentId = topParentId;
 1026        }
 1027
 01028        if (reader.TryGetString(index++, out var audioString) && Enum.TryParse<ProgramAudioEntity>(audioString, out var 
 1029        {
 01030            entity.Audio = audioType;
 1031        }
 1032
 01033        if (reader.TryGetString(index++, out var serviceName))
 1034        {
 01035            entity.ExternalServiceId = serviceName;
 1036        }
 1037
 01038        if (reader.TryGetBoolean(index++, out var isInMixedFolder))
 1039        {
 01040            entity.IsInMixedFolder = isInMixedFolder;
 1041        }
 1042
 01043        if (reader.TryReadDateTime(index++, out var dateLastSaved))
 1044        {
 01045            entity.DateLastSaved = dateLastSaved;
 1046        }
 1047
 01048        if (reader.TryGetString(index++, out var lockedFields))
 1049        {
 01050            entity.LockedFields = lockedFields.Split('|').Select(Enum.Parse<MetadataField>)
 01051                .Select(e => new BaseItemMetadataField()
 01052                {
 01053                    Id = (int)e,
 01054                    Item = entity,
 01055                    ItemId = entity.Id
 01056                })
 01057                .ToArray();
 1058        }
 1059
 01060        if (reader.TryGetString(index++, out var studios))
 1061        {
 01062            entity.Studios = studios;
 1063        }
 1064
 01065        if (reader.TryGetString(index++, out var tags))
 1066        {
 01067            entity.Tags = tags;
 1068        }
 1069
 01070        if (reader.TryGetString(index++, out var trailerTypes))
 1071        {
 01072            entity.TrailerTypes = trailerTypes.Split('|').Select(Enum.Parse<TrailerType>)
 01073                .Select(e => new BaseItemTrailerType()
 01074                {
 01075                    Id = (int)e,
 01076                    Item = entity,
 01077                    ItemId = entity.Id
 01078                })
 01079                .ToArray();
 1080        }
 1081
 01082        if (reader.TryGetString(index++, out var originalTitle))
 1083        {
 01084            entity.OriginalTitle = originalTitle;
 1085        }
 1086
 01087        if (reader.TryGetString(index++, out var primaryVersionId))
 1088        {
 01089            entity.PrimaryVersionId = primaryVersionId;
 1090        }
 1091
 01092        if (reader.TryReadDateTime(index++, out var dateLastMediaAdded))
 1093        {
 01094            entity.DateLastMediaAdded = dateLastMediaAdded;
 1095        }
 1096
 01097        if (reader.TryGetString(index++, out var album))
 1098        {
 01099            entity.Album = album;
 1100        }
 1101
 01102        if (reader.TryGetSingle(index++, out var lUFS))
 1103        {
 01104            entity.LUFS = lUFS;
 1105        }
 1106
 01107        if (reader.TryGetSingle(index++, out var normalizationGain))
 1108        {
 01109            entity.NormalizationGain = normalizationGain;
 1110        }
 1111
 01112        if (reader.TryGetSingle(index++, out var criticRating))
 1113        {
 01114            entity.CriticRating = criticRating;
 1115        }
 1116
 01117        if (reader.TryGetBoolean(index++, out var isVirtualItem))
 1118        {
 01119            entity.IsVirtualItem = isVirtualItem;
 1120        }
 1121
 01122        if (reader.TryGetString(index++, out var seriesName))
 1123        {
 01124            entity.SeriesName = seriesName;
 1125        }
 1126
 01127        var userDataKeys = new List<string>();
 01128        if (reader.TryGetString(index++, out var directUserDataKey))
 1129        {
 01130            userDataKeys.Add(directUserDataKey);
 1131        }
 1132
 01133        if (reader.TryGetString(index++, out var seasonName))
 1134        {
 01135            entity.SeasonName = seasonName;
 1136        }
 1137
 01138        if (reader.TryGetGuid(index++, out var seasonId))
 1139        {
 01140            entity.SeasonId = seasonId;
 1141        }
 1142
 01143        if (reader.TryGetGuid(index++, out var seriesId))
 1144        {
 01145            entity.SeriesId = seriesId;
 1146        }
 1147
 01148        if (reader.TryGetString(index++, out var presentationUniqueKey))
 1149        {
 01150            entity.PresentationUniqueKey = presentationUniqueKey;
 1151        }
 1152
 01153        if (reader.TryGetInt32(index++, out var parentalRating))
 1154        {
 01155            entity.InheritedParentalRatingValue = parentalRating;
 1156        }
 1157
 01158        if (reader.TryGetString(index++, out var externalSeriesId))
 1159        {
 01160            entity.ExternalSeriesId = externalSeriesId;
 1161        }
 1162
 01163        if (reader.TryGetString(index++, out var tagLine))
 1164        {
 01165            entity.Tagline = tagLine;
 1166        }
 1167
 01168        if (reader.TryGetString(index++, out var providerIds))
 1169        {
 01170            entity.Provider = providerIds.Split('|').Select(e => e.Split("=")).Where(e => e.Length >= 2)
 01171            .Select(e => new BaseItemProvider()
 01172            {
 01173                Item = null!,
 01174                ProviderId = e[0],
 01175                ProviderValue = string.Join('|', e.Skip(1))
 01176            })
 01177            .DistinctBy(e => e.ProviderId)
 01178            .ToArray();
 1179        }
 1180
 01181        if (reader.TryGetString(index++, out var imageInfos))
 1182        {
 01183            entity.Images = DeserializeImages(imageInfos).Select(f => Map(entity.Id, f)).ToArray();
 1184        }
 1185
 01186        if (reader.TryGetString(index++, out var productionLocations))
 1187        {
 01188            entity.ProductionLocations = productionLocations;
 1189        }
 1190
 01191        if (reader.TryGetString(index++, out var extraIds))
 1192        {
 01193            entity.ExtraIds = extraIds;
 1194        }
 1195
 01196        if (reader.TryGetInt32(index++, out var totalBitrate))
 1197        {
 01198            entity.TotalBitrate = totalBitrate;
 1199        }
 1200
 01201        if (reader.TryGetString(index++, out var extraTypeString) && Enum.TryParse<BaseItemExtraType>(extraTypeString, o
 1202        {
 01203            entity.ExtraType = extraType;
 1204        }
 1205
 01206        if (reader.TryGetString(index++, out var artists))
 1207        {
 01208            entity.Artists = artists;
 1209        }
 1210
 01211        if (reader.TryGetString(index++, out var albumArtists))
 1212        {
 01213            entity.AlbumArtists = albumArtists;
 1214        }
 1215
 01216        if (reader.TryGetString(index++, out var externalId))
 1217        {
 01218            entity.ExternalId = externalId;
 1219        }
 1220
 01221        if (reader.TryGetString(index++, out var seriesPresentationUniqueKey))
 1222        {
 01223            entity.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
 1224        }
 1225
 01226        if (reader.TryGetString(index++, out var showId))
 1227        {
 01228            entity.ShowId = showId;
 1229        }
 1230
 01231        if (reader.TryGetString(index++, out var ownerId))
 1232        {
 01233            entity.OwnerId = ownerId;
 1234        }
 1235
 01236        if (reader.TryGetString(index++, out var mediaType))
 1237        {
 01238            entity.MediaType = mediaType;
 1239        }
 1240
 01241        if (reader.TryGetString(index++, out var sortName))
 1242        {
 01243            entity.SortName = sortName;
 1244        }
 1245
 01246        if (reader.TryGetString(index++, out var cleanName))
 1247        {
 01248            entity.CleanName = cleanName;
 1249        }
 1250
 01251        if (reader.TryGetString(index++, out var unratedType))
 1252        {
 01253            entity.UnratedType = unratedType;
 1254        }
 1255
 01256        if (reader.TryGetBoolean(index++, out var isFolder))
 1257        {
 01258            entity.IsFolder = isFolder;
 1259        }
 1260
 01261        var baseItem = BaseItemRepository.DeserializeBaseItem(entity, _logger, null, false);
 01262        if (baseItem is not null)
 1263        {
 01264            var dataKeys = baseItem.GetUserDataKeys();
 01265            userDataKeys.AddRange(dataKeys);
 1266        }
 1267
 01268        return (entity, userDataKeys.ToArray());
 1269    }
 1270
 1271    private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
 1272    {
 01273        return new BaseItemImageInfo()
 01274        {
 01275            ItemId = baseItemId,
 01276            Id = Guid.NewGuid(),
 01277            Path = e.Path,
 01278            Blurhash = e.BlurHash is not null ? Encoding.UTF8.GetBytes(e.BlurHash) : null,
 01279            DateModified = e.DateModified,
 01280            Height = e.Height,
 01281            Width = e.Width,
 01282            ImageType = (ImageInfoImageType)e.Type,
 01283            Item = null!
 01284        };
 1285    }
 1286
 1287    internal ItemImageInfo[] DeserializeImages(string value)
 1288    {
 01289        if (string.IsNullOrWhiteSpace(value))
 1290        {
 01291            return Array.Empty<ItemImageInfo>();
 1292        }
 1293
 1294        // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the data
 01295        var valueSpan = value.AsSpan();
 01296        var count = valueSpan.Count('|') + 1;
 1297
 01298        var position = 0;
 01299        var result = new ItemImageInfo[count];
 01300        foreach (var part in valueSpan.Split('|'))
 1301        {
 01302            var image = ItemImageInfoFromValueString(part);
 1303
 01304            if (image is not null)
 1305            {
 01306                result[position++] = image;
 1307            }
 1308        }
 1309
 01310        if (position == count)
 1311        {
 01312            return result;
 1313        }
 1314
 01315        if (position == 0)
 1316        {
 01317            return Array.Empty<ItemImageInfo>();
 1318        }
 1319
 1320        // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array.
 01321        return result[..position];
 1322    }
 1323
 1324    internal ItemImageInfo? ItemImageInfoFromValueString(ReadOnlySpan<char> value)
 1325    {
 1326        const char Delimiter = '*';
 1327
 01328        var nextSegment = value.IndexOf(Delimiter);
 01329        if (nextSegment == -1)
 1330        {
 01331            return null;
 1332        }
 1333
 01334        ReadOnlySpan<char> path = value[..nextSegment];
 01335        value = value[(nextSegment + 1)..];
 01336        nextSegment = value.IndexOf(Delimiter);
 01337        if (nextSegment == -1)
 1338        {
 01339            return null;
 1340        }
 1341
 01342        ReadOnlySpan<char> dateModified = value[..nextSegment];
 01343        value = value[(nextSegment + 1)..];
 01344        nextSegment = value.IndexOf(Delimiter);
 01345        if (nextSegment == -1)
 1346        {
 01347            nextSegment = value.Length;
 1348        }
 1349
 01350        ReadOnlySpan<char> imageType = value[..nextSegment];
 1351
 01352        var image = new ItemImageInfo
 01353        {
 01354            Path = path.ToString()
 01355        };
 1356
 01357        if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks)
 01358            && ticks >= DateTime.MinValue.Ticks
 01359            && ticks <= DateTime.MaxValue.Ticks)
 1360        {
 01361            image.DateModified = new DateTime(ticks, DateTimeKind.Utc);
 1362        }
 1363        else
 1364        {
 01365            return null;
 1366        }
 1367
 01368        if (Enum.TryParse(imageType, true, out ImageType type))
 1369        {
 01370            image.Type = type;
 1371        }
 1372        else
 1373        {
 01374            return null;
 1375        }
 1376
 1377        // Optional parameters: width*height*blurhash
 01378        if (nextSegment + 1 < value.Length - 1)
 1379        {
 01380            value = value[(nextSegment + 1)..];
 01381            nextSegment = value.IndexOf(Delimiter);
 01382            if (nextSegment == -1 || nextSegment == value.Length)
 1383            {
 01384                return image;
 1385            }
 1386
 01387            ReadOnlySpan<char> widthSpan = value[..nextSegment];
 1388
 01389            value = value[(nextSegment + 1)..];
 01390            nextSegment = value.IndexOf(Delimiter);
 01391            if (nextSegment == -1)
 1392            {
 01393                nextSegment = value.Length;
 1394            }
 1395
 01396            ReadOnlySpan<char> heightSpan = value[..nextSegment];
 1397
 01398            if (int.TryParse(widthSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var width)
 01399                && int.TryParse(heightSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var height))
 1400            {
 01401                image.Width = width;
 01402                image.Height = height;
 1403            }
 1404
 01405            if (nextSegment < value.Length - 1)
 1406            {
 01407                value = value[(nextSegment + 1)..];
 01408                var length = value.Length;
 1409
 01410                Span<char> blurHashSpan = stackalloc char[length];
 01411                for (int i = 0; i < length; i++)
 1412                {
 01413                    var c = value[i];
 01414                    blurHashSpan[i] = c switch
 01415                    {
 01416                        '/' => Delimiter,
 01417                        '\\' => '|',
 01418                        _ => c
 01419                    };
 1420                }
 1421
 01422                image.BlurHash = new string(blurHashSpan);
 1423            }
 1424        }
 1425
 01426        return image;
 1427    }
 1428
 1429    private class TrackedMigrationStep : IDisposable
 1430    {
 1431        private readonly string _operationName;
 1432        private readonly ILogger _logger;
 1433        private readonly Stopwatch _operationTimer;
 1434        private bool _disposed;
 1435
 1436        public TrackedMigrationStep(string operationName, ILogger logger)
 1437        {
 01438            _operationName = operationName;
 01439            _logger = logger;
 01440            _operationTimer = Stopwatch.StartNew();
 01441            logger.LogInformation("Start {OperationName}", operationName);
 01442        }
 1443
 1444        public bool Disposed
 1445        {
 01446            get => _disposed;
 01447            set => _disposed = value;
 1448        }
 1449
 1450        public virtual void Dispose()
 1451        {
 01452            if (Disposed)
 1453            {
 01454                return;
 1455            }
 1456
 01457            Disposed = true;
 01458            _logger.LogInformation("{OperationName} took '{Time}'", _operationName, _operationTimer.Elapsed);
 01459        }
 1460    }
 1461
 1462    private sealed class DatabaseMigrationStep : TrackedMigrationStep
 1463    {
 01464        public DatabaseMigrationStep(JellyfinDbContext jellyfinDbContext, string operationName, ILogger logger) : base(o
 1465        {
 1466            JellyfinDbContext = jellyfinDbContext;
 01467        }
 1468
 1469        public JellyfinDbContext JellyfinDbContext { get; }
 1470
 1471        public override void Dispose()
 1472        {
 01473            if (Disposed)
 1474            {
 01475                return;
 1476            }
 1477
 01478            JellyfinDbContext.Dispose();
 01479            base.Dispose();
 01480        }
 1481    }
 1482}