< 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: 633
Coverable lines: 633
Total lines: 1467
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 362
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 8/22/2025 - 12:09:44 AM Line coverage: 0% (0/614) Branch coverage: 0% (0/342) Total lines: 13969/20/2025 - 12:11:30 AM Line coverage: 0% (0/625) Branch coverage: 0% (0/346) Total lines: 14369/25/2025 - 12:11:18 AM Line coverage: 0% (0/626) Branch coverage: 0% (0/348) Total lines: 14379/26/2025 - 12:11:03 AM Line coverage: 0% (0/627) Branch coverage: 0% (0/350) Total lines: 14429/29/2025 - 12:11:37 AM Line coverage: 0% (0/634) Branch coverage: 0% (0/362) Total lines: 146911/18/2025 - 12:11:25 AM Line coverage: 0% (0/633) Branch coverage: 0% (0/362) Total lines: 1467 8/22/2025 - 12:09:44 AM Line coverage: 0% (0/614) Branch coverage: 0% (0/342) Total lines: 13969/20/2025 - 12:11:30 AM Line coverage: 0% (0/625) Branch coverage: 0% (0/346) Total lines: 14369/25/2025 - 12:11:18 AM Line coverage: 0% (0/626) Branch coverage: 0% (0/348) Total lines: 14379/26/2025 - 12:11:03 AM Line coverage: 0% (0/627) Branch coverage: 0% (0/350) Total lines: 14429/29/2025 - 12:11:37 AM Line coverage: 0% (0/634) Branch coverage: 0% (0/362) Total lines: 146911/18/2025 - 12:11:25 AM Line coverage: 0% (0/633) Branch coverage: 0% (0/362) Total lines: 1467

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%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        {
 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        _logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old");
 0468        File.Move(libraryDbPath, libraryDbPath + ".old", true);
 0469    }
 470
 471    private DatabaseMigrationStep GetPreparedDbContext(string operationName)
 472    {
 0473        var dbContext = _provider.CreateDbContext();
 0474        dbContext.ChangeTracker.AutoDetectChangesEnabled = false;
 0475        dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
 0476        return new DatabaseMigrationStep(dbContext, operationName, _logger);
 477    }
 478
 479    internal static UserData? GetUserData(User[] users, SqliteDataReader dto, HashSet<int> userIdBlacklist, ILogger logg
 480    {
 0481        var internalUserId = dto.GetInt32(1);
 0482        if (userIdBlacklist.Contains(internalUserId))
 483        {
 0484            return null;
 485        }
 486
 0487        var user = users.FirstOrDefault(e => e.InternalId == internalUserId);
 0488        if (user is null)
 489        {
 0490            userIdBlacklist.Add(internalUserId);
 491
 0492            logger.LogError("Tried to find user with index '{Idx}' but there are only '{MaxIdx}' users.", internalUserId
 0493            return null;
 494        }
 495
 0496        var oldKey = dto.GetString(0);
 497
 0498        return new UserData()
 0499        {
 0500            ItemId = Guid.NewGuid(),
 0501            CustomDataKey = oldKey,
 0502            UserId = user.Id,
 0503            Rating = dto.IsDBNull(2) ? null : dto.GetDouble(2),
 0504            Played = dto.GetBoolean(3),
 0505            PlayCount = dto.GetInt32(4),
 0506            IsFavorite = dto.GetBoolean(5),
 0507            PlaybackPositionTicks = dto.GetInt64(6),
 0508            LastPlayedDate = dto.IsDBNull(7) ? null : dto.GetDateTime(7),
 0509            AudioStreamIndex = dto.IsDBNull(8) ? null : dto.GetInt32(8),
 0510            SubtitleStreamIndex = dto.IsDBNull(9) ? null : dto.GetInt32(9),
 0511            Likes = null,
 0512            User = null!,
 0513            Item = null!
 0514        };
 515    }
 516
 517    private AncestorId GetAncestorId(SqliteDataReader reader)
 518    {
 0519        return new AncestorId()
 0520        {
 0521            ItemId = reader.GetGuid(0),
 0522            ParentItemId = reader.GetGuid(1),
 0523            Item = null!,
 0524            ParentItem = null!
 0525        };
 526    }
 527
 528    /// <summary>
 529    /// Gets the chapter.
 530    /// </summary>
 531    /// <param name="reader">The reader.</param>
 532    /// <returns>ChapterInfo.</returns>
 533    private Chapter GetChapter(SqliteDataReader reader)
 534    {
 0535        var chapter = new Chapter
 0536        {
 0537            StartPositionTicks = reader.GetInt64(1),
 0538            ChapterIndex = reader.GetInt32(5),
 0539            Item = null!,
 0540            ItemId = reader.GetGuid(0),
 0541        };
 542
 0543        if (reader.TryGetString(2, out var chapterName))
 544        {
 0545            chapter.Name = chapterName;
 546        }
 547
 0548        if (reader.TryGetString(3, out var imagePath))
 549        {
 0550            chapter.ImagePath = imagePath;
 551        }
 552
 0553        if (reader.TryReadDateTime(4, out var imageDateModified))
 554        {
 0555            chapter.ImageDateModified = imageDateModified;
 556        }
 557
 0558        return chapter;
 559    }
 560
 561    private ItemValue GetItemValue(SqliteDataReader reader)
 562    {
 0563        return new ItemValue
 0564        {
 0565            ItemValueId = Guid.NewGuid(),
 0566            Type = (ItemValueType)reader.GetInt32(1),
 0567            Value = reader.GetString(2),
 0568            CleanValue = reader.GetString(3),
 0569        };
 570    }
 571
 572    private People GetPerson(SqliteDataReader reader)
 573    {
 0574        var item = new People
 0575        {
 0576            Id = Guid.NewGuid(),
 0577            Name = reader.GetString(1),
 0578        };
 579
 0580        if (reader.TryGetString(3, out var type))
 581        {
 0582            item.PersonType = type;
 583        }
 584
 0585        return item;
 586    }
 587
 588    /// <summary>
 589    /// Gets the media stream.
 590    /// </summary>
 591    /// <param name="reader">The reader.</param>
 592    /// <returns>MediaStream.</returns>
 593    private MediaStreamInfo GetMediaStream(SqliteDataReader reader)
 594    {
 0595        var item = new MediaStreamInfo
 0596        {
 0597            StreamIndex = reader.GetInt32(1),
 0598            StreamType = Enum.Parse<MediaStreamTypeEntity>(reader.GetString(2)),
 0599            Item = null!,
 0600            ItemId = reader.GetGuid(0),
 0601            AspectRatio = null!,
 0602            ChannelLayout = null!,
 0603            Codec = null!,
 0604            IsInterlaced = false,
 0605            Language = null!,
 0606            Path = null!,
 0607            Profile = null!,
 0608        };
 609
 0610        if (reader.TryGetString(3, out var codec))
 611        {
 0612            item.Codec = codec;
 613        }
 614
 0615        if (reader.TryGetString(4, out var language))
 616        {
 0617            item.Language = language;
 618        }
 619
 0620        if (reader.TryGetString(5, out var channelLayout))
 621        {
 0622            item.ChannelLayout = channelLayout;
 623        }
 624
 0625        if (reader.TryGetString(6, out var profile))
 626        {
 0627            item.Profile = profile;
 628        }
 629
 0630        if (reader.TryGetString(7, out var aspectRatio))
 631        {
 0632            item.AspectRatio = aspectRatio;
 633        }
 634
 0635        if (reader.TryGetString(8, out var path))
 636        {
 0637            item.Path = path;
 638        }
 639
 0640        item.IsInterlaced = reader.GetBoolean(9);
 641
 0642        if (reader.TryGetInt32(10, out var bitrate))
 643        {
 0644            item.BitRate = bitrate;
 645        }
 646
 0647        if (reader.TryGetInt32(11, out var channels))
 648        {
 0649            item.Channels = channels;
 650        }
 651
 0652        if (reader.TryGetInt32(12, out var sampleRate))
 653        {
 0654            item.SampleRate = sampleRate;
 655        }
 656
 0657        item.IsDefault = reader.GetBoolean(13);
 0658        item.IsForced = reader.GetBoolean(14);
 0659        item.IsExternal = reader.GetBoolean(15);
 660
 0661        if (reader.TryGetInt32(16, out var width))
 662        {
 0663            item.Width = width;
 664        }
 665
 0666        if (reader.TryGetInt32(17, out var height))
 667        {
 0668            item.Height = height;
 669        }
 670
 0671        if (reader.TryGetSingle(18, out var averageFrameRate))
 672        {
 0673            item.AverageFrameRate = averageFrameRate;
 674        }
 675
 0676        if (reader.TryGetSingle(19, out var realFrameRate))
 677        {
 0678            item.RealFrameRate = realFrameRate;
 679        }
 680
 0681        if (reader.TryGetSingle(20, out var level))
 682        {
 0683            item.Level = level;
 684        }
 685
 0686        if (reader.TryGetString(21, out var pixelFormat))
 687        {
 0688            item.PixelFormat = pixelFormat;
 689        }
 690
 0691        if (reader.TryGetInt32(22, out var bitDepth))
 692        {
 0693            item.BitDepth = bitDepth;
 694        }
 695
 0696        if (reader.TryGetBoolean(23, out var isAnamorphic))
 697        {
 0698            item.IsAnamorphic = isAnamorphic;
 699        }
 700
 0701        if (reader.TryGetInt32(24, out var refFrames))
 702        {
 0703            item.RefFrames = refFrames;
 704        }
 705
 0706        if (reader.TryGetString(25, out var codecTag))
 707        {
 0708            item.CodecTag = codecTag;
 709        }
 710
 0711        if (reader.TryGetString(26, out var comment))
 712        {
 0713            item.Comment = comment;
 714        }
 715
 0716        if (reader.TryGetString(27, out var nalLengthSize))
 717        {
 0718            item.NalLengthSize = nalLengthSize;
 719        }
 720
 0721        if (reader.TryGetBoolean(28, out var isAVC))
 722        {
 0723            item.IsAvc = isAVC;
 724        }
 725
 0726        if (reader.TryGetString(29, out var title))
 727        {
 0728            item.Title = title;
 729        }
 730
 0731        if (reader.TryGetString(30, out var timeBase))
 732        {
 0733            item.TimeBase = timeBase;
 734        }
 735
 0736        if (reader.TryGetString(31, out var codecTimeBase))
 737        {
 0738            item.CodecTimeBase = codecTimeBase;
 739        }
 740
 0741        if (reader.TryGetString(32, out var colorPrimaries))
 742        {
 0743            item.ColorPrimaries = colorPrimaries;
 744        }
 745
 0746        if (reader.TryGetString(33, out var colorSpace))
 747        {
 0748            item.ColorSpace = colorSpace;
 749        }
 750
 0751        if (reader.TryGetString(34, out var colorTransfer))
 752        {
 0753            item.ColorTransfer = colorTransfer;
 754        }
 755
 0756        if (reader.TryGetInt32(35, out var dvVersionMajor))
 757        {
 0758            item.DvVersionMajor = dvVersionMajor;
 759        }
 760
 0761        if (reader.TryGetInt32(36, out var dvVersionMinor))
 762        {
 0763            item.DvVersionMinor = dvVersionMinor;
 764        }
 765
 0766        if (reader.TryGetInt32(37, out var dvProfile))
 767        {
 0768            item.DvProfile = dvProfile;
 769        }
 770
 0771        if (reader.TryGetInt32(38, out var dvLevel))
 772        {
 0773            item.DvLevel = dvLevel;
 774        }
 775
 0776        if (reader.TryGetInt32(39, out var rpuPresentFlag))
 777        {
 0778            item.RpuPresentFlag = rpuPresentFlag;
 779        }
 780
 0781        if (reader.TryGetInt32(40, out var elPresentFlag))
 782        {
 0783            item.ElPresentFlag = elPresentFlag;
 784        }
 785
 0786        if (reader.TryGetInt32(41, out var blPresentFlag))
 787        {
 0788            item.BlPresentFlag = blPresentFlag;
 789        }
 790
 0791        if (reader.TryGetInt32(42, out var dvBlSignalCompatibilityId))
 792        {
 0793            item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId;
 794        }
 795
 0796        item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result;
 797
 798        // if (reader.TryGetInt32(44, out var rotation))
 799        // {
 800        //     item.Rotation = rotation;
 801        // }
 802
 0803        return item;
 804    }
 805
 806    /// <summary>
 807    /// Gets the attachment.
 808    /// </summary>
 809    /// <param name="reader">The reader.</param>
 810    /// <returns>MediaAttachment.</returns>
 811    private AttachmentStreamInfo GetMediaAttachment(SqliteDataReader reader)
 812    {
 0813        var item = new AttachmentStreamInfo
 0814        {
 0815            Index = reader.GetInt32(1),
 0816            Item = null!,
 0817            ItemId = reader.GetGuid(0),
 0818        };
 819
 0820        if (reader.TryGetString(2, out var codec))
 821        {
 0822            item.Codec = codec;
 823        }
 824
 0825        if (reader.TryGetString(3, out var codecTag))
 826        {
 0827            item.CodecTag = codecTag;
 828        }
 829
 0830        if (reader.TryGetString(4, out var comment))
 831        {
 0832            item.Comment = comment;
 833        }
 834
 0835        if (reader.TryGetString(5, out var fileName))
 836        {
 0837            item.Filename = fileName;
 838        }
 839
 0840        if (reader.TryGetString(6, out var mimeType))
 841        {
 0842            item.MimeType = mimeType;
 843        }
 844
 0845        return item;
 846    }
 847
 848    private (BaseItemEntity BaseItem, string[] LegacyUserDataKey) GetItem(SqliteDataReader reader)
 849    {
 0850        var entity = new BaseItemEntity()
 0851        {
 0852            Id = reader.GetGuid(0),
 0853            Type = reader.GetString(1),
 0854        };
 855
 0856        var index = 2;
 857
 0858        if (reader.TryGetString(index++, out var data))
 859        {
 0860            entity.Data = data;
 861        }
 862
 0863        if (reader.TryReadDateTime(index++, out var startDate))
 864        {
 0865            entity.StartDate = startDate;
 866        }
 867
 0868        if (reader.TryReadDateTime(index++, out var endDate))
 869        {
 0870            entity.EndDate = endDate;
 871        }
 872
 0873        if (reader.TryGetGuid(index++, out var guid))
 874        {
 0875            entity.ChannelId = guid;
 876        }
 877
 0878        if (reader.TryGetBoolean(index++, out var isMovie))
 879        {
 0880            entity.IsMovie = isMovie;
 881        }
 882
 0883        if (reader.TryGetBoolean(index++, out var isSeries))
 884        {
 0885            entity.IsSeries = isSeries;
 886        }
 887
 0888        if (reader.TryGetString(index++, out var episodeTitle))
 889        {
 0890            entity.EpisodeTitle = episodeTitle;
 891        }
 892
 0893        if (reader.TryGetBoolean(index++, out var isRepeat))
 894        {
 0895            entity.IsRepeat = isRepeat;
 896        }
 897
 0898        if (reader.TryGetSingle(index++, out var communityRating))
 899        {
 0900            entity.CommunityRating = communityRating;
 901        }
 902
 0903        if (reader.TryGetString(index++, out var customRating))
 904        {
 0905            entity.CustomRating = customRating;
 906        }
 907
 0908        if (reader.TryGetInt32(index++, out var indexNumber))
 909        {
 0910            entity.IndexNumber = indexNumber;
 911        }
 912
 0913        if (reader.TryGetBoolean(index++, out var isLocked))
 914        {
 0915            entity.IsLocked = isLocked;
 916        }
 917
 0918        if (reader.TryGetString(index++, out var preferredMetadataLanguage))
 919        {
 0920            entity.PreferredMetadataLanguage = preferredMetadataLanguage;
 921        }
 922
 0923        if (reader.TryGetString(index++, out var preferredMetadataCountryCode))
 924        {
 0925            entity.PreferredMetadataCountryCode = preferredMetadataCountryCode;
 926        }
 927
 0928        if (reader.TryGetInt32(index++, out var width))
 929        {
 0930            entity.Width = width;
 931        }
 932
 0933        if (reader.TryGetInt32(index++, out var height))
 934        {
 0935            entity.Height = height;
 936        }
 937
 0938        if (reader.TryReadDateTime(index++, out var dateLastRefreshed))
 939        {
 0940            entity.DateLastRefreshed = dateLastRefreshed;
 941        }
 942
 0943        if (reader.TryGetString(index++, out var name))
 944        {
 0945            entity.Name = name;
 946        }
 947
 0948        if (reader.TryGetString(index++, out var restorePath))
 949        {
 0950            entity.Path = restorePath;
 951        }
 952
 0953        if (reader.TryReadDateTime(index++, out var premiereDate))
 954        {
 0955            entity.PremiereDate = premiereDate;
 956        }
 957
 0958        if (reader.TryGetString(index++, out var overview))
 959        {
 0960            entity.Overview = overview;
 961        }
 962
 0963        if (reader.TryGetInt32(index++, out var parentIndexNumber))
 964        {
 0965            entity.ParentIndexNumber = parentIndexNumber;
 966        }
 967
 0968        if (reader.TryGetInt32(index++, out var productionYear))
 969        {
 0970            entity.ProductionYear = productionYear;
 971        }
 972
 0973        if (reader.TryGetString(index++, out var officialRating))
 974        {
 0975            entity.OfficialRating = officialRating;
 976        }
 977
 0978        if (reader.TryGetString(index++, out var forcedSortName))
 979        {
 0980            entity.ForcedSortName = forcedSortName;
 981        }
 982
 0983        if (reader.TryGetInt64(index++, out var runTimeTicks))
 984        {
 0985            entity.RunTimeTicks = runTimeTicks;
 986        }
 987
 0988        if (reader.TryGetInt64(index++, out var size))
 989        {
 0990            entity.Size = size;
 991        }
 992
 0993        if (reader.TryReadDateTime(index++, out var dateCreated))
 994        {
 0995            entity.DateCreated = dateCreated;
 996        }
 997
 0998        if (reader.TryReadDateTime(index++, out var dateModified))
 999        {
 01000            entity.DateModified = dateModified;
 1001        }
 1002
 01003        if (reader.TryGetString(index++, out var genres))
 1004        {
 01005            entity.Genres = genres;
 1006        }
 1007
 01008        if (reader.TryGetGuid(index++, out var parentId))
 1009        {
 01010            entity.ParentId = parentId;
 1011        }
 1012
 01013        if (reader.TryGetGuid(index++, out var topParentId))
 1014        {
 01015            entity.TopParentId = topParentId;
 1016        }
 1017
 01018        if (reader.TryGetString(index++, out var audioString) && Enum.TryParse<ProgramAudioEntity>(audioString, out var 
 1019        {
 01020            entity.Audio = audioType;
 1021        }
 1022
 01023        if (reader.TryGetString(index++, out var serviceName))
 1024        {
 01025            entity.ExternalServiceId = serviceName;
 1026        }
 1027
 01028        if (reader.TryGetBoolean(index++, out var isInMixedFolder))
 1029        {
 01030            entity.IsInMixedFolder = isInMixedFolder;
 1031        }
 1032
 01033        if (reader.TryReadDateTime(index++, out var dateLastSaved))
 1034        {
 01035            entity.DateLastSaved = dateLastSaved;
 1036        }
 1037
 01038        if (reader.TryGetString(index++, out var lockedFields))
 1039        {
 01040            entity.LockedFields = lockedFields.Split('|').Select(Enum.Parse<MetadataField>)
 01041                .Select(e => new BaseItemMetadataField()
 01042                {
 01043                    Id = (int)e,
 01044                    Item = entity,
 01045                    ItemId = entity.Id
 01046                })
 01047                .ToArray();
 1048        }
 1049
 01050        if (reader.TryGetString(index++, out var studios))
 1051        {
 01052            entity.Studios = studios;
 1053        }
 1054
 01055        if (reader.TryGetString(index++, out var tags))
 1056        {
 01057            entity.Tags = tags;
 1058        }
 1059
 01060        if (reader.TryGetString(index++, out var trailerTypes))
 1061        {
 01062            entity.TrailerTypes = trailerTypes.Split('|').Select(Enum.Parse<TrailerType>)
 01063                .Select(e => new BaseItemTrailerType()
 01064                {
 01065                    Id = (int)e,
 01066                    Item = entity,
 01067                    ItemId = entity.Id
 01068                })
 01069                .ToArray();
 1070        }
 1071
 01072        if (reader.TryGetString(index++, out var originalTitle))
 1073        {
 01074            entity.OriginalTitle = originalTitle;
 1075        }
 1076
 01077        if (reader.TryGetString(index++, out var primaryVersionId))
 1078        {
 01079            entity.PrimaryVersionId = primaryVersionId;
 1080        }
 1081
 01082        if (reader.TryReadDateTime(index++, out var dateLastMediaAdded))
 1083        {
 01084            entity.DateLastMediaAdded = dateLastMediaAdded;
 1085        }
 1086
 01087        if (reader.TryGetString(index++, out var album))
 1088        {
 01089            entity.Album = album;
 1090        }
 1091
 01092        if (reader.TryGetSingle(index++, out var lUFS))
 1093        {
 01094            entity.LUFS = lUFS;
 1095        }
 1096
 01097        if (reader.TryGetSingle(index++, out var normalizationGain))
 1098        {
 01099            entity.NormalizationGain = normalizationGain;
 1100        }
 1101
 01102        if (reader.TryGetSingle(index++, out var criticRating))
 1103        {
 01104            entity.CriticRating = criticRating;
 1105        }
 1106
 01107        if (reader.TryGetBoolean(index++, out var isVirtualItem))
 1108        {
 01109            entity.IsVirtualItem = isVirtualItem;
 1110        }
 1111
 01112        if (reader.TryGetString(index++, out var seriesName))
 1113        {
 01114            entity.SeriesName = seriesName;
 1115        }
 1116
 01117        var userDataKeys = new List<string>();
 01118        if (reader.TryGetString(index++, out var directUserDataKey))
 1119        {
 01120            userDataKeys.Add(directUserDataKey);
 1121        }
 1122
 01123        if (reader.TryGetString(index++, out var seasonName))
 1124        {
 01125            entity.SeasonName = seasonName;
 1126        }
 1127
 01128        if (reader.TryGetGuid(index++, out var seasonId))
 1129        {
 01130            entity.SeasonId = seasonId;
 1131        }
 1132
 01133        if (reader.TryGetGuid(index++, out var seriesId))
 1134        {
 01135            entity.SeriesId = seriesId;
 1136        }
 1137
 01138        if (reader.TryGetString(index++, out var presentationUniqueKey))
 1139        {
 01140            entity.PresentationUniqueKey = presentationUniqueKey;
 1141        }
 1142
 01143        if (reader.TryGetInt32(index++, out var parentalRating))
 1144        {
 01145            entity.InheritedParentalRatingValue = parentalRating;
 1146        }
 1147
 01148        if (reader.TryGetString(index++, out var externalSeriesId))
 1149        {
 01150            entity.ExternalSeriesId = externalSeriesId;
 1151        }
 1152
 01153        if (reader.TryGetString(index++, out var tagLine))
 1154        {
 01155            entity.Tagline = tagLine;
 1156        }
 1157
 01158        if (reader.TryGetString(index++, out var providerIds))
 1159        {
 01160            entity.Provider = providerIds.Split('|').Select(e => e.Split("=")).Where(e => e.Length >= 2)
 01161            .Select(e => new BaseItemProvider()
 01162            {
 01163                Item = null!,
 01164                ProviderId = e[0],
 01165                ProviderValue = string.Join('|', e.Skip(1))
 01166            }).ToArray();
 1167        }
 1168
 01169        if (reader.TryGetString(index++, out var imageInfos))
 1170        {
 01171            entity.Images = DeserializeImages(imageInfos).Select(f => Map(entity.Id, f)).ToArray();
 1172        }
 1173
 01174        if (reader.TryGetString(index++, out var productionLocations))
 1175        {
 01176            entity.ProductionLocations = productionLocations;
 1177        }
 1178
 01179        if (reader.TryGetString(index++, out var extraIds))
 1180        {
 01181            entity.ExtraIds = extraIds;
 1182        }
 1183
 01184        if (reader.TryGetInt32(index++, out var totalBitrate))
 1185        {
 01186            entity.TotalBitrate = totalBitrate;
 1187        }
 1188
 01189        if (reader.TryGetString(index++, out var extraTypeString) && Enum.TryParse<BaseItemExtraType>(extraTypeString, o
 1190        {
 01191            entity.ExtraType = extraType;
 1192        }
 1193
 01194        if (reader.TryGetString(index++, out var artists))
 1195        {
 01196            entity.Artists = artists;
 1197        }
 1198
 01199        if (reader.TryGetString(index++, out var albumArtists))
 1200        {
 01201            entity.AlbumArtists = albumArtists;
 1202        }
 1203
 01204        if (reader.TryGetString(index++, out var externalId))
 1205        {
 01206            entity.ExternalId = externalId;
 1207        }
 1208
 01209        if (reader.TryGetString(index++, out var seriesPresentationUniqueKey))
 1210        {
 01211            entity.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
 1212        }
 1213
 01214        if (reader.TryGetString(index++, out var showId))
 1215        {
 01216            entity.ShowId = showId;
 1217        }
 1218
 01219        if (reader.TryGetString(index++, out var ownerId))
 1220        {
 01221            entity.OwnerId = ownerId;
 1222        }
 1223
 01224        if (reader.TryGetString(index++, out var mediaType))
 1225        {
 01226            entity.MediaType = mediaType;
 1227        }
 1228
 01229        if (reader.TryGetString(index++, out var sortName))
 1230        {
 01231            entity.SortName = sortName;
 1232        }
 1233
 01234        if (reader.TryGetString(index++, out var cleanName))
 1235        {
 01236            entity.CleanName = cleanName;
 1237        }
 1238
 01239        if (reader.TryGetString(index++, out var unratedType))
 1240        {
 01241            entity.UnratedType = unratedType;
 1242        }
 1243
 01244        if (reader.TryGetBoolean(index++, out var isFolder))
 1245        {
 01246            entity.IsFolder = isFolder;
 1247        }
 1248
 01249        var baseItem = BaseItemRepository.DeserializeBaseItem(entity, _logger, null, false);
 01250        var dataKeys = baseItem.GetUserDataKeys();
 01251        userDataKeys.AddRange(dataKeys);
 1252
 01253        return (entity, userDataKeys.ToArray());
 1254    }
 1255
 1256    private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
 1257    {
 01258        return new BaseItemImageInfo()
 01259        {
 01260            ItemId = baseItemId,
 01261            Id = Guid.NewGuid(),
 01262            Path = e.Path,
 01263            Blurhash = e.BlurHash is not null ? Encoding.UTF8.GetBytes(e.BlurHash) : null,
 01264            DateModified = e.DateModified,
 01265            Height = e.Height,
 01266            Width = e.Width,
 01267            ImageType = (ImageInfoImageType)e.Type,
 01268            Item = null!
 01269        };
 1270    }
 1271
 1272    internal ItemImageInfo[] DeserializeImages(string value)
 1273    {
 01274        if (string.IsNullOrWhiteSpace(value))
 1275        {
 01276            return Array.Empty<ItemImageInfo>();
 1277        }
 1278
 1279        // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the data
 01280        var valueSpan = value.AsSpan();
 01281        var count = valueSpan.Count('|') + 1;
 1282
 01283        var position = 0;
 01284        var result = new ItemImageInfo[count];
 01285        foreach (var part in valueSpan.Split('|'))
 1286        {
 01287            var image = ItemImageInfoFromValueString(part);
 1288
 01289            if (image is not null)
 1290            {
 01291                result[position++] = image;
 1292            }
 1293        }
 1294
 01295        if (position == count)
 1296        {
 01297            return result;
 1298        }
 1299
 01300        if (position == 0)
 1301        {
 01302            return Array.Empty<ItemImageInfo>();
 1303        }
 1304
 1305        // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array.
 01306        return result[..position];
 1307    }
 1308
 1309    internal ItemImageInfo? ItemImageInfoFromValueString(ReadOnlySpan<char> value)
 1310    {
 1311        const char Delimiter = '*';
 1312
 01313        var nextSegment = value.IndexOf(Delimiter);
 01314        if (nextSegment == -1)
 1315        {
 01316            return null;
 1317        }
 1318
 01319        ReadOnlySpan<char> path = value[..nextSegment];
 01320        value = value[(nextSegment + 1)..];
 01321        nextSegment = value.IndexOf(Delimiter);
 01322        if (nextSegment == -1)
 1323        {
 01324            return null;
 1325        }
 1326
 01327        ReadOnlySpan<char> dateModified = value[..nextSegment];
 01328        value = value[(nextSegment + 1)..];
 01329        nextSegment = value.IndexOf(Delimiter);
 01330        if (nextSegment == -1)
 1331        {
 01332            nextSegment = value.Length;
 1333        }
 1334
 01335        ReadOnlySpan<char> imageType = value[..nextSegment];
 1336
 01337        var image = new ItemImageInfo
 01338        {
 01339            Path = path.ToString()
 01340        };
 1341
 01342        if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks)
 01343            && ticks >= DateTime.MinValue.Ticks
 01344            && ticks <= DateTime.MaxValue.Ticks)
 1345        {
 01346            image.DateModified = new DateTime(ticks, DateTimeKind.Utc);
 1347        }
 1348        else
 1349        {
 01350            return null;
 1351        }
 1352
 01353        if (Enum.TryParse(imageType, true, out ImageType type))
 1354        {
 01355            image.Type = type;
 1356        }
 1357        else
 1358        {
 01359            return null;
 1360        }
 1361
 1362        // Optional parameters: width*height*blurhash
 01363        if (nextSegment + 1 < value.Length - 1)
 1364        {
 01365            value = value[(nextSegment + 1)..];
 01366            nextSegment = value.IndexOf(Delimiter);
 01367            if (nextSegment == -1 || nextSegment == value.Length)
 1368            {
 01369                return image;
 1370            }
 1371
 01372            ReadOnlySpan<char> widthSpan = value[..nextSegment];
 1373
 01374            value = value[(nextSegment + 1)..];
 01375            nextSegment = value.IndexOf(Delimiter);
 01376            if (nextSegment == -1)
 1377            {
 01378                nextSegment = value.Length;
 1379            }
 1380
 01381            ReadOnlySpan<char> heightSpan = value[..nextSegment];
 1382
 01383            if (int.TryParse(widthSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var width)
 01384                && int.TryParse(heightSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var height))
 1385            {
 01386                image.Width = width;
 01387                image.Height = height;
 1388            }
 1389
 01390            if (nextSegment < value.Length - 1)
 1391            {
 01392                value = value[(nextSegment + 1)..];
 01393                var length = value.Length;
 1394
 01395                Span<char> blurHashSpan = stackalloc char[length];
 01396                for (int i = 0; i < length; i++)
 1397                {
 01398                    var c = value[i];
 01399                    blurHashSpan[i] = c switch
 01400                    {
 01401                        '/' => Delimiter,
 01402                        '\\' => '|',
 01403                        _ => c
 01404                    };
 1405                }
 1406
 01407                image.BlurHash = new string(blurHashSpan);
 1408            }
 1409        }
 1410
 01411        return image;
 1412    }
 1413
 1414    private class TrackedMigrationStep : IDisposable
 1415    {
 1416        private readonly string _operationName;
 1417        private readonly ILogger _logger;
 1418        private readonly Stopwatch _operationTimer;
 1419        private bool _disposed;
 1420
 1421        public TrackedMigrationStep(string operationName, ILogger logger)
 1422        {
 01423            _operationName = operationName;
 01424            _logger = logger;
 01425            _operationTimer = Stopwatch.StartNew();
 01426            logger.LogInformation("Start {OperationName}", operationName);
 01427        }
 1428
 1429        public bool Disposed
 1430        {
 01431            get => _disposed;
 01432            set => _disposed = value;
 1433        }
 1434
 1435        public virtual void Dispose()
 1436        {
 01437            if (Disposed)
 1438            {
 01439                return;
 1440            }
 1441
 01442            Disposed = true;
 01443            _logger.LogInformation("{OperationName} took '{Time}'", _operationName, _operationTimer.Elapsed);
 01444        }
 1445    }
 1446
 1447    private sealed class DatabaseMigrationStep : TrackedMigrationStep
 1448    {
 01449        public DatabaseMigrationStep(JellyfinDbContext jellyfinDbContext, string operationName, ILogger logger) : base(o
 1450        {
 1451            JellyfinDbContext = jellyfinDbContext;
 01452        }
 1453
 1454        public JellyfinDbContext JellyfinDbContext { get; }
 1455
 1456        public override void Dispose()
 1457        {
 01458            if (Disposed)
 1459            {
 01460                return;
 1461            }
 1462
 01463            JellyfinDbContext.Dispose();
 01464            base.Dispose();
 01465        }
 1466    }
 1467}