< 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: 634
Coverable lines: 634
Total lines: 1469
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

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                baseItemIds.Clear();
 387
 0388                foreach (var item in peopleCache)
 389                {
 0390                    operation.JellyfinDbContext.Peoples.Add(item.Value.Person);
 0391                    operation.JellyfinDbContext.PeopleBaseItemMap.AddRange(item.Value.Items.DistinctBy(e => (e.ItemId, e
 392                }
 393
 0394                peopleCache.Clear();
 0395            }
 396
 0397            using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.Peoples.Local.Count} People entries an
 398            {
 0399                operation.JellyfinDbContext.SaveChanges();
 0400            }
 401        }
 402
 0403        using (var operation = GetPreparedDbContext("Moving Chapters"))
 404        {
 405            const string chapterQuery =
 406            """
 407            SELECT ItemId,StartPositionTicks,Name,ImagePath,ImageDateModified,ChapterIndex from Chapters2
 408            WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = Chapters2.ItemId)
 409            """;
 410
 0411            using (new TrackedMigrationStep("Loading Chapters", _logger))
 412            {
 0413                foreach (SqliteDataReader dto in connection.Query(chapterQuery))
 414                {
 0415                    var chapter = GetChapter(dto);
 0416                    if (!baseItemIds.Contains(chapter.ItemId))
 417                    {
 418                        continue;
 419                    }
 420
 0421                    operation.JellyfinDbContext.Chapters.Add(chapter);
 422                }
 423            }
 424
 0425            using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.Chapters.Local.Count} Chapters entries
 426            {
 0427                operation.JellyfinDbContext.SaveChanges();
 0428            }
 429        }
 430
 0431        using (var operation = GetPreparedDbContext("Moving AncestorIds"))
 432        {
 433            const string ancestorIdsQuery =
 434            """
 435            SELECT ItemId, AncestorId, AncestorIdText FROM AncestorIds
 436            WHERE
 437            EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.ItemId)
 438            AND
 439            EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.AncestorId)
 440            """;
 441
 0442            using (new TrackedMigrationStep("Loading AncestorIds", _logger))
 443            {
 0444                foreach (SqliteDataReader dto in connection.Query(ancestorIdsQuery))
 445                {
 0446                    var ancestorId = GetAncestorId(dto);
 0447                    if (!baseItemIds.Contains(ancestorId.ItemId) || !baseItemIds.Contains(ancestorId.ParentItemId))
 448                    {
 449                        continue;
 450                    }
 451
 0452                    operation.JellyfinDbContext.AncestorIds.Add(ancestorId);
 453                }
 454            }
 455
 0456            using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.AncestorIds.Local.Count} AncestorId en
 457            {
 0458                operation.JellyfinDbContext.SaveChanges();
 0459            }
 460        }
 461
 0462        connection.Close();
 463
 0464        _logger.LogInformation("Migration of the Library.db done.");
 0465        _logger.LogInformation("Migrating Library db took {0}.", fullOperationTimer.Elapsed);
 466
 0467        SqliteConnection.ClearAllPools();
 468
 0469        _logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old");
 0470        File.Move(libraryDbPath, libraryDbPath + ".old", true);
 0471    }
 472
 473    private DatabaseMigrationStep GetPreparedDbContext(string operationName)
 474    {
 0475        var dbContext = _provider.CreateDbContext();
 0476        dbContext.ChangeTracker.AutoDetectChangesEnabled = false;
 0477        dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
 0478        return new DatabaseMigrationStep(dbContext, operationName, _logger);
 479    }
 480
 481    internal static UserData? GetUserData(User[] users, SqliteDataReader dto, HashSet<int> userIdBlacklist, ILogger logg
 482    {
 0483        var internalUserId = dto.GetInt32(1);
 0484        if (userIdBlacklist.Contains(internalUserId))
 485        {
 0486            return null;
 487        }
 488
 0489        var user = users.FirstOrDefault(e => e.InternalId == internalUserId);
 0490        if (user is null)
 491        {
 0492            userIdBlacklist.Add(internalUserId);
 493
 0494            logger.LogError("Tried to find user with index '{Idx}' but there are only '{MaxIdx}' users.", internalUserId
 0495            return null;
 496        }
 497
 0498        var oldKey = dto.GetString(0);
 499
 0500        return new UserData()
 0501        {
 0502            ItemId = Guid.NewGuid(),
 0503            CustomDataKey = oldKey,
 0504            UserId = user.Id,
 0505            Rating = dto.IsDBNull(2) ? null : dto.GetDouble(2),
 0506            Played = dto.GetBoolean(3),
 0507            PlayCount = dto.GetInt32(4),
 0508            IsFavorite = dto.GetBoolean(5),
 0509            PlaybackPositionTicks = dto.GetInt64(6),
 0510            LastPlayedDate = dto.IsDBNull(7) ? null : dto.GetDateTime(7),
 0511            AudioStreamIndex = dto.IsDBNull(8) ? null : dto.GetInt32(8),
 0512            SubtitleStreamIndex = dto.IsDBNull(9) ? null : dto.GetInt32(9),
 0513            Likes = null,
 0514            User = null!,
 0515            Item = null!
 0516        };
 517    }
 518
 519    private AncestorId GetAncestorId(SqliteDataReader reader)
 520    {
 0521        return new AncestorId()
 0522        {
 0523            ItemId = reader.GetGuid(0),
 0524            ParentItemId = reader.GetGuid(1),
 0525            Item = null!,
 0526            ParentItem = null!
 0527        };
 528    }
 529
 530    /// <summary>
 531    /// Gets the chapter.
 532    /// </summary>
 533    /// <param name="reader">The reader.</param>
 534    /// <returns>ChapterInfo.</returns>
 535    private Chapter GetChapter(SqliteDataReader reader)
 536    {
 0537        var chapter = new Chapter
 0538        {
 0539            StartPositionTicks = reader.GetInt64(1),
 0540            ChapterIndex = reader.GetInt32(5),
 0541            Item = null!,
 0542            ItemId = reader.GetGuid(0),
 0543        };
 544
 0545        if (reader.TryGetString(2, out var chapterName))
 546        {
 0547            chapter.Name = chapterName;
 548        }
 549
 0550        if (reader.TryGetString(3, out var imagePath))
 551        {
 0552            chapter.ImagePath = imagePath;
 553        }
 554
 0555        if (reader.TryReadDateTime(4, out var imageDateModified))
 556        {
 0557            chapter.ImageDateModified = imageDateModified;
 558        }
 559
 0560        return chapter;
 561    }
 562
 563    private ItemValue GetItemValue(SqliteDataReader reader)
 564    {
 0565        return new ItemValue
 0566        {
 0567            ItemValueId = Guid.NewGuid(),
 0568            Type = (ItemValueType)reader.GetInt32(1),
 0569            Value = reader.GetString(2),
 0570            CleanValue = reader.GetString(3),
 0571        };
 572    }
 573
 574    private People GetPerson(SqliteDataReader reader)
 575    {
 0576        var item = new People
 0577        {
 0578            Id = Guid.NewGuid(),
 0579            Name = reader.GetString(1),
 0580        };
 581
 0582        if (reader.TryGetString(3, out var type))
 583        {
 0584            item.PersonType = type;
 585        }
 586
 0587        return item;
 588    }
 589
 590    /// <summary>
 591    /// Gets the media stream.
 592    /// </summary>
 593    /// <param name="reader">The reader.</param>
 594    /// <returns>MediaStream.</returns>
 595    private MediaStreamInfo GetMediaStream(SqliteDataReader reader)
 596    {
 0597        var item = new MediaStreamInfo
 0598        {
 0599            StreamIndex = reader.GetInt32(1),
 0600            StreamType = Enum.Parse<MediaStreamTypeEntity>(reader.GetString(2)),
 0601            Item = null!,
 0602            ItemId = reader.GetGuid(0),
 0603            AspectRatio = null!,
 0604            ChannelLayout = null!,
 0605            Codec = null!,
 0606            IsInterlaced = false,
 0607            Language = null!,
 0608            Path = null!,
 0609            Profile = null!,
 0610        };
 611
 0612        if (reader.TryGetString(3, out var codec))
 613        {
 0614            item.Codec = codec;
 615        }
 616
 0617        if (reader.TryGetString(4, out var language))
 618        {
 0619            item.Language = language;
 620        }
 621
 0622        if (reader.TryGetString(5, out var channelLayout))
 623        {
 0624            item.ChannelLayout = channelLayout;
 625        }
 626
 0627        if (reader.TryGetString(6, out var profile))
 628        {
 0629            item.Profile = profile;
 630        }
 631
 0632        if (reader.TryGetString(7, out var aspectRatio))
 633        {
 0634            item.AspectRatio = aspectRatio;
 635        }
 636
 0637        if (reader.TryGetString(8, out var path))
 638        {
 0639            item.Path = path;
 640        }
 641
 0642        item.IsInterlaced = reader.GetBoolean(9);
 643
 0644        if (reader.TryGetInt32(10, out var bitrate))
 645        {
 0646            item.BitRate = bitrate;
 647        }
 648
 0649        if (reader.TryGetInt32(11, out var channels))
 650        {
 0651            item.Channels = channels;
 652        }
 653
 0654        if (reader.TryGetInt32(12, out var sampleRate))
 655        {
 0656            item.SampleRate = sampleRate;
 657        }
 658
 0659        item.IsDefault = reader.GetBoolean(13);
 0660        item.IsForced = reader.GetBoolean(14);
 0661        item.IsExternal = reader.GetBoolean(15);
 662
 0663        if (reader.TryGetInt32(16, out var width))
 664        {
 0665            item.Width = width;
 666        }
 667
 0668        if (reader.TryGetInt32(17, out var height))
 669        {
 0670            item.Height = height;
 671        }
 672
 0673        if (reader.TryGetSingle(18, out var averageFrameRate))
 674        {
 0675            item.AverageFrameRate = averageFrameRate;
 676        }
 677
 0678        if (reader.TryGetSingle(19, out var realFrameRate))
 679        {
 0680            item.RealFrameRate = realFrameRate;
 681        }
 682
 0683        if (reader.TryGetSingle(20, out var level))
 684        {
 0685            item.Level = level;
 686        }
 687
 0688        if (reader.TryGetString(21, out var pixelFormat))
 689        {
 0690            item.PixelFormat = pixelFormat;
 691        }
 692
 0693        if (reader.TryGetInt32(22, out var bitDepth))
 694        {
 0695            item.BitDepth = bitDepth;
 696        }
 697
 0698        if (reader.TryGetBoolean(23, out var isAnamorphic))
 699        {
 0700            item.IsAnamorphic = isAnamorphic;
 701        }
 702
 0703        if (reader.TryGetInt32(24, out var refFrames))
 704        {
 0705            item.RefFrames = refFrames;
 706        }
 707
 0708        if (reader.TryGetString(25, out var codecTag))
 709        {
 0710            item.CodecTag = codecTag;
 711        }
 712
 0713        if (reader.TryGetString(26, out var comment))
 714        {
 0715            item.Comment = comment;
 716        }
 717
 0718        if (reader.TryGetString(27, out var nalLengthSize))
 719        {
 0720            item.NalLengthSize = nalLengthSize;
 721        }
 722
 0723        if (reader.TryGetBoolean(28, out var isAVC))
 724        {
 0725            item.IsAvc = isAVC;
 726        }
 727
 0728        if (reader.TryGetString(29, out var title))
 729        {
 0730            item.Title = title;
 731        }
 732
 0733        if (reader.TryGetString(30, out var timeBase))
 734        {
 0735            item.TimeBase = timeBase;
 736        }
 737
 0738        if (reader.TryGetString(31, out var codecTimeBase))
 739        {
 0740            item.CodecTimeBase = codecTimeBase;
 741        }
 742
 0743        if (reader.TryGetString(32, out var colorPrimaries))
 744        {
 0745            item.ColorPrimaries = colorPrimaries;
 746        }
 747
 0748        if (reader.TryGetString(33, out var colorSpace))
 749        {
 0750            item.ColorSpace = colorSpace;
 751        }
 752
 0753        if (reader.TryGetString(34, out var colorTransfer))
 754        {
 0755            item.ColorTransfer = colorTransfer;
 756        }
 757
 0758        if (reader.TryGetInt32(35, out var dvVersionMajor))
 759        {
 0760            item.DvVersionMajor = dvVersionMajor;
 761        }
 762
 0763        if (reader.TryGetInt32(36, out var dvVersionMinor))
 764        {
 0765            item.DvVersionMinor = dvVersionMinor;
 766        }
 767
 0768        if (reader.TryGetInt32(37, out var dvProfile))
 769        {
 0770            item.DvProfile = dvProfile;
 771        }
 772
 0773        if (reader.TryGetInt32(38, out var dvLevel))
 774        {
 0775            item.DvLevel = dvLevel;
 776        }
 777
 0778        if (reader.TryGetInt32(39, out var rpuPresentFlag))
 779        {
 0780            item.RpuPresentFlag = rpuPresentFlag;
 781        }
 782
 0783        if (reader.TryGetInt32(40, out var elPresentFlag))
 784        {
 0785            item.ElPresentFlag = elPresentFlag;
 786        }
 787
 0788        if (reader.TryGetInt32(41, out var blPresentFlag))
 789        {
 0790            item.BlPresentFlag = blPresentFlag;
 791        }
 792
 0793        if (reader.TryGetInt32(42, out var dvBlSignalCompatibilityId))
 794        {
 0795            item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId;
 796        }
 797
 0798        item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result;
 799
 800        // if (reader.TryGetInt32(44, out var rotation))
 801        // {
 802        //     item.Rotation = rotation;
 803        // }
 804
 0805        return item;
 806    }
 807
 808    /// <summary>
 809    /// Gets the attachment.
 810    /// </summary>
 811    /// <param name="reader">The reader.</param>
 812    /// <returns>MediaAttachment.</returns>
 813    private AttachmentStreamInfo GetMediaAttachment(SqliteDataReader reader)
 814    {
 0815        var item = new AttachmentStreamInfo
 0816        {
 0817            Index = reader.GetInt32(1),
 0818            Item = null!,
 0819            ItemId = reader.GetGuid(0),
 0820        };
 821
 0822        if (reader.TryGetString(2, out var codec))
 823        {
 0824            item.Codec = codec;
 825        }
 826
 0827        if (reader.TryGetString(3, out var codecTag))
 828        {
 0829            item.CodecTag = codecTag;
 830        }
 831
 0832        if (reader.TryGetString(4, out var comment))
 833        {
 0834            item.Comment = comment;
 835        }
 836
 0837        if (reader.TryGetString(5, out var fileName))
 838        {
 0839            item.Filename = fileName;
 840        }
 841
 0842        if (reader.TryGetString(6, out var mimeType))
 843        {
 0844            item.MimeType = mimeType;
 845        }
 846
 0847        return item;
 848    }
 849
 850    private (BaseItemEntity BaseItem, string[] LegacyUserDataKey) GetItem(SqliteDataReader reader)
 851    {
 0852        var entity = new BaseItemEntity()
 0853        {
 0854            Id = reader.GetGuid(0),
 0855            Type = reader.GetString(1),
 0856        };
 857
 0858        var index = 2;
 859
 0860        if (reader.TryGetString(index++, out var data))
 861        {
 0862            entity.Data = data;
 863        }
 864
 0865        if (reader.TryReadDateTime(index++, out var startDate))
 866        {
 0867            entity.StartDate = startDate;
 868        }
 869
 0870        if (reader.TryReadDateTime(index++, out var endDate))
 871        {
 0872            entity.EndDate = endDate;
 873        }
 874
 0875        if (reader.TryGetGuid(index++, out var guid))
 876        {
 0877            entity.ChannelId = guid;
 878        }
 879
 0880        if (reader.TryGetBoolean(index++, out var isMovie))
 881        {
 0882            entity.IsMovie = isMovie;
 883        }
 884
 0885        if (reader.TryGetBoolean(index++, out var isSeries))
 886        {
 0887            entity.IsSeries = isSeries;
 888        }
 889
 0890        if (reader.TryGetString(index++, out var episodeTitle))
 891        {
 0892            entity.EpisodeTitle = episodeTitle;
 893        }
 894
 0895        if (reader.TryGetBoolean(index++, out var isRepeat))
 896        {
 0897            entity.IsRepeat = isRepeat;
 898        }
 899
 0900        if (reader.TryGetSingle(index++, out var communityRating))
 901        {
 0902            entity.CommunityRating = communityRating;
 903        }
 904
 0905        if (reader.TryGetString(index++, out var customRating))
 906        {
 0907            entity.CustomRating = customRating;
 908        }
 909
 0910        if (reader.TryGetInt32(index++, out var indexNumber))
 911        {
 0912            entity.IndexNumber = indexNumber;
 913        }
 914
 0915        if (reader.TryGetBoolean(index++, out var isLocked))
 916        {
 0917            entity.IsLocked = isLocked;
 918        }
 919
 0920        if (reader.TryGetString(index++, out var preferredMetadataLanguage))
 921        {
 0922            entity.PreferredMetadataLanguage = preferredMetadataLanguage;
 923        }
 924
 0925        if (reader.TryGetString(index++, out var preferredMetadataCountryCode))
 926        {
 0927            entity.PreferredMetadataCountryCode = preferredMetadataCountryCode;
 928        }
 929
 0930        if (reader.TryGetInt32(index++, out var width))
 931        {
 0932            entity.Width = width;
 933        }
 934
 0935        if (reader.TryGetInt32(index++, out var height))
 936        {
 0937            entity.Height = height;
 938        }
 939
 0940        if (reader.TryReadDateTime(index++, out var dateLastRefreshed))
 941        {
 0942            entity.DateLastRefreshed = dateLastRefreshed;
 943        }
 944
 0945        if (reader.TryGetString(index++, out var name))
 946        {
 0947            entity.Name = name;
 948        }
 949
 0950        if (reader.TryGetString(index++, out var restorePath))
 951        {
 0952            entity.Path = restorePath;
 953        }
 954
 0955        if (reader.TryReadDateTime(index++, out var premiereDate))
 956        {
 0957            entity.PremiereDate = premiereDate;
 958        }
 959
 0960        if (reader.TryGetString(index++, out var overview))
 961        {
 0962            entity.Overview = overview;
 963        }
 964
 0965        if (reader.TryGetInt32(index++, out var parentIndexNumber))
 966        {
 0967            entity.ParentIndexNumber = parentIndexNumber;
 968        }
 969
 0970        if (reader.TryGetInt32(index++, out var productionYear))
 971        {
 0972            entity.ProductionYear = productionYear;
 973        }
 974
 0975        if (reader.TryGetString(index++, out var officialRating))
 976        {
 0977            entity.OfficialRating = officialRating;
 978        }
 979
 0980        if (reader.TryGetString(index++, out var forcedSortName))
 981        {
 0982            entity.ForcedSortName = forcedSortName;
 983        }
 984
 0985        if (reader.TryGetInt64(index++, out var runTimeTicks))
 986        {
 0987            entity.RunTimeTicks = runTimeTicks;
 988        }
 989
 0990        if (reader.TryGetInt64(index++, out var size))
 991        {
 0992            entity.Size = size;
 993        }
 994
 0995        if (reader.TryReadDateTime(index++, out var dateCreated))
 996        {
 0997            entity.DateCreated = dateCreated;
 998        }
 999
 01000        if (reader.TryReadDateTime(index++, out var dateModified))
 1001        {
 01002            entity.DateModified = dateModified;
 1003        }
 1004
 01005        if (reader.TryGetString(index++, out var genres))
 1006        {
 01007            entity.Genres = genres;
 1008        }
 1009
 01010        if (reader.TryGetGuid(index++, out var parentId))
 1011        {
 01012            entity.ParentId = parentId;
 1013        }
 1014
 01015        if (reader.TryGetGuid(index++, out var topParentId))
 1016        {
 01017            entity.TopParentId = topParentId;
 1018        }
 1019
 01020        if (reader.TryGetString(index++, out var audioString) && Enum.TryParse<ProgramAudioEntity>(audioString, out var 
 1021        {
 01022            entity.Audio = audioType;
 1023        }
 1024
 01025        if (reader.TryGetString(index++, out var serviceName))
 1026        {
 01027            entity.ExternalServiceId = serviceName;
 1028        }
 1029
 01030        if (reader.TryGetBoolean(index++, out var isInMixedFolder))
 1031        {
 01032            entity.IsInMixedFolder = isInMixedFolder;
 1033        }
 1034
 01035        if (reader.TryReadDateTime(index++, out var dateLastSaved))
 1036        {
 01037            entity.DateLastSaved = dateLastSaved;
 1038        }
 1039
 01040        if (reader.TryGetString(index++, out var lockedFields))
 1041        {
 01042            entity.LockedFields = lockedFields.Split('|').Select(Enum.Parse<MetadataField>)
 01043                .Select(e => new BaseItemMetadataField()
 01044                {
 01045                    Id = (int)e,
 01046                    Item = entity,
 01047                    ItemId = entity.Id
 01048                })
 01049                .ToArray();
 1050        }
 1051
 01052        if (reader.TryGetString(index++, out var studios))
 1053        {
 01054            entity.Studios = studios;
 1055        }
 1056
 01057        if (reader.TryGetString(index++, out var tags))
 1058        {
 01059            entity.Tags = tags;
 1060        }
 1061
 01062        if (reader.TryGetString(index++, out var trailerTypes))
 1063        {
 01064            entity.TrailerTypes = trailerTypes.Split('|').Select(Enum.Parse<TrailerType>)
 01065                .Select(e => new BaseItemTrailerType()
 01066                {
 01067                    Id = (int)e,
 01068                    Item = entity,
 01069                    ItemId = entity.Id
 01070                })
 01071                .ToArray();
 1072        }
 1073
 01074        if (reader.TryGetString(index++, out var originalTitle))
 1075        {
 01076            entity.OriginalTitle = originalTitle;
 1077        }
 1078
 01079        if (reader.TryGetString(index++, out var primaryVersionId))
 1080        {
 01081            entity.PrimaryVersionId = primaryVersionId;
 1082        }
 1083
 01084        if (reader.TryReadDateTime(index++, out var dateLastMediaAdded))
 1085        {
 01086            entity.DateLastMediaAdded = dateLastMediaAdded;
 1087        }
 1088
 01089        if (reader.TryGetString(index++, out var album))
 1090        {
 01091            entity.Album = album;
 1092        }
 1093
 01094        if (reader.TryGetSingle(index++, out var lUFS))
 1095        {
 01096            entity.LUFS = lUFS;
 1097        }
 1098
 01099        if (reader.TryGetSingle(index++, out var normalizationGain))
 1100        {
 01101            entity.NormalizationGain = normalizationGain;
 1102        }
 1103
 01104        if (reader.TryGetSingle(index++, out var criticRating))
 1105        {
 01106            entity.CriticRating = criticRating;
 1107        }
 1108
 01109        if (reader.TryGetBoolean(index++, out var isVirtualItem))
 1110        {
 01111            entity.IsVirtualItem = isVirtualItem;
 1112        }
 1113
 01114        if (reader.TryGetString(index++, out var seriesName))
 1115        {
 01116            entity.SeriesName = seriesName;
 1117        }
 1118
 01119        var userDataKeys = new List<string>();
 01120        if (reader.TryGetString(index++, out var directUserDataKey))
 1121        {
 01122            userDataKeys.Add(directUserDataKey);
 1123        }
 1124
 01125        if (reader.TryGetString(index++, out var seasonName))
 1126        {
 01127            entity.SeasonName = seasonName;
 1128        }
 1129
 01130        if (reader.TryGetGuid(index++, out var seasonId))
 1131        {
 01132            entity.SeasonId = seasonId;
 1133        }
 1134
 01135        if (reader.TryGetGuid(index++, out var seriesId))
 1136        {
 01137            entity.SeriesId = seriesId;
 1138        }
 1139
 01140        if (reader.TryGetString(index++, out var presentationUniqueKey))
 1141        {
 01142            entity.PresentationUniqueKey = presentationUniqueKey;
 1143        }
 1144
 01145        if (reader.TryGetInt32(index++, out var parentalRating))
 1146        {
 01147            entity.InheritedParentalRatingValue = parentalRating;
 1148        }
 1149
 01150        if (reader.TryGetString(index++, out var externalSeriesId))
 1151        {
 01152            entity.ExternalSeriesId = externalSeriesId;
 1153        }
 1154
 01155        if (reader.TryGetString(index++, out var tagLine))
 1156        {
 01157            entity.Tagline = tagLine;
 1158        }
 1159
 01160        if (reader.TryGetString(index++, out var providerIds))
 1161        {
 01162            entity.Provider = providerIds.Split('|').Select(e => e.Split("=")).Where(e => e.Length >= 2)
 01163            .Select(e => new BaseItemProvider()
 01164            {
 01165                Item = null!,
 01166                ProviderId = e[0],
 01167                ProviderValue = string.Join('|', e.Skip(1))
 01168            }).ToArray();
 1169        }
 1170
 01171        if (reader.TryGetString(index++, out var imageInfos))
 1172        {
 01173            entity.Images = DeserializeImages(imageInfos).Select(f => Map(entity.Id, f)).ToArray();
 1174        }
 1175
 01176        if (reader.TryGetString(index++, out var productionLocations))
 1177        {
 01178            entity.ProductionLocations = productionLocations;
 1179        }
 1180
 01181        if (reader.TryGetString(index++, out var extraIds))
 1182        {
 01183            entity.ExtraIds = extraIds;
 1184        }
 1185
 01186        if (reader.TryGetInt32(index++, out var totalBitrate))
 1187        {
 01188            entity.TotalBitrate = totalBitrate;
 1189        }
 1190
 01191        if (reader.TryGetString(index++, out var extraTypeString) && Enum.TryParse<BaseItemExtraType>(extraTypeString, o
 1192        {
 01193            entity.ExtraType = extraType;
 1194        }
 1195
 01196        if (reader.TryGetString(index++, out var artists))
 1197        {
 01198            entity.Artists = artists;
 1199        }
 1200
 01201        if (reader.TryGetString(index++, out var albumArtists))
 1202        {
 01203            entity.AlbumArtists = albumArtists;
 1204        }
 1205
 01206        if (reader.TryGetString(index++, out var externalId))
 1207        {
 01208            entity.ExternalId = externalId;
 1209        }
 1210
 01211        if (reader.TryGetString(index++, out var seriesPresentationUniqueKey))
 1212        {
 01213            entity.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
 1214        }
 1215
 01216        if (reader.TryGetString(index++, out var showId))
 1217        {
 01218            entity.ShowId = showId;
 1219        }
 1220
 01221        if (reader.TryGetString(index++, out var ownerId))
 1222        {
 01223            entity.OwnerId = ownerId;
 1224        }
 1225
 01226        if (reader.TryGetString(index++, out var mediaType))
 1227        {
 01228            entity.MediaType = mediaType;
 1229        }
 1230
 01231        if (reader.TryGetString(index++, out var sortName))
 1232        {
 01233            entity.SortName = sortName;
 1234        }
 1235
 01236        if (reader.TryGetString(index++, out var cleanName))
 1237        {
 01238            entity.CleanName = cleanName;
 1239        }
 1240
 01241        if (reader.TryGetString(index++, out var unratedType))
 1242        {
 01243            entity.UnratedType = unratedType;
 1244        }
 1245
 01246        if (reader.TryGetBoolean(index++, out var isFolder))
 1247        {
 01248            entity.IsFolder = isFolder;
 1249        }
 1250
 01251        var baseItem = BaseItemRepository.DeserializeBaseItem(entity, _logger, null, false);
 01252        var dataKeys = baseItem.GetUserDataKeys();
 01253        userDataKeys.AddRange(dataKeys);
 1254
 01255        return (entity, userDataKeys.ToArray());
 1256    }
 1257
 1258    private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
 1259    {
 01260        return new BaseItemImageInfo()
 01261        {
 01262            ItemId = baseItemId,
 01263            Id = Guid.NewGuid(),
 01264            Path = e.Path,
 01265            Blurhash = e.BlurHash is not null ? Encoding.UTF8.GetBytes(e.BlurHash) : null,
 01266            DateModified = e.DateModified,
 01267            Height = e.Height,
 01268            Width = e.Width,
 01269            ImageType = (ImageInfoImageType)e.Type,
 01270            Item = null!
 01271        };
 1272    }
 1273
 1274    internal ItemImageInfo[] DeserializeImages(string value)
 1275    {
 01276        if (string.IsNullOrWhiteSpace(value))
 1277        {
 01278            return Array.Empty<ItemImageInfo>();
 1279        }
 1280
 1281        // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the data
 01282        var valueSpan = value.AsSpan();
 01283        var count = valueSpan.Count('|') + 1;
 1284
 01285        var position = 0;
 01286        var result = new ItemImageInfo[count];
 01287        foreach (var part in valueSpan.Split('|'))
 1288        {
 01289            var image = ItemImageInfoFromValueString(part);
 1290
 01291            if (image is not null)
 1292            {
 01293                result[position++] = image;
 1294            }
 1295        }
 1296
 01297        if (position == count)
 1298        {
 01299            return result;
 1300        }
 1301
 01302        if (position == 0)
 1303        {
 01304            return Array.Empty<ItemImageInfo>();
 1305        }
 1306
 1307        // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array.
 01308        return result[..position];
 1309    }
 1310
 1311    internal ItemImageInfo? ItemImageInfoFromValueString(ReadOnlySpan<char> value)
 1312    {
 1313        const char Delimiter = '*';
 1314
 01315        var nextSegment = value.IndexOf(Delimiter);
 01316        if (nextSegment == -1)
 1317        {
 01318            return null;
 1319        }
 1320
 01321        ReadOnlySpan<char> path = value[..nextSegment];
 01322        value = value[(nextSegment + 1)..];
 01323        nextSegment = value.IndexOf(Delimiter);
 01324        if (nextSegment == -1)
 1325        {
 01326            return null;
 1327        }
 1328
 01329        ReadOnlySpan<char> dateModified = value[..nextSegment];
 01330        value = value[(nextSegment + 1)..];
 01331        nextSegment = value.IndexOf(Delimiter);
 01332        if (nextSegment == -1)
 1333        {
 01334            nextSegment = value.Length;
 1335        }
 1336
 01337        ReadOnlySpan<char> imageType = value[..nextSegment];
 1338
 01339        var image = new ItemImageInfo
 01340        {
 01341            Path = path.ToString()
 01342        };
 1343
 01344        if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks)
 01345            && ticks >= DateTime.MinValue.Ticks
 01346            && ticks <= DateTime.MaxValue.Ticks)
 1347        {
 01348            image.DateModified = new DateTime(ticks, DateTimeKind.Utc);
 1349        }
 1350        else
 1351        {
 01352            return null;
 1353        }
 1354
 01355        if (Enum.TryParse(imageType, true, out ImageType type))
 1356        {
 01357            image.Type = type;
 1358        }
 1359        else
 1360        {
 01361            return null;
 1362        }
 1363
 1364        // Optional parameters: width*height*blurhash
 01365        if (nextSegment + 1 < value.Length - 1)
 1366        {
 01367            value = value[(nextSegment + 1)..];
 01368            nextSegment = value.IndexOf(Delimiter);
 01369            if (nextSegment == -1 || nextSegment == value.Length)
 1370            {
 01371                return image;
 1372            }
 1373
 01374            ReadOnlySpan<char> widthSpan = value[..nextSegment];
 1375
 01376            value = value[(nextSegment + 1)..];
 01377            nextSegment = value.IndexOf(Delimiter);
 01378            if (nextSegment == -1)
 1379            {
 01380                nextSegment = value.Length;
 1381            }
 1382
 01383            ReadOnlySpan<char> heightSpan = value[..nextSegment];
 1384
 01385            if (int.TryParse(widthSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var width)
 01386                && int.TryParse(heightSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var height))
 1387            {
 01388                image.Width = width;
 01389                image.Height = height;
 1390            }
 1391
 01392            if (nextSegment < value.Length - 1)
 1393            {
 01394                value = value[(nextSegment + 1)..];
 01395                var length = value.Length;
 1396
 01397                Span<char> blurHashSpan = stackalloc char[length];
 01398                for (int i = 0; i < length; i++)
 1399                {
 01400                    var c = value[i];
 01401                    blurHashSpan[i] = c switch
 01402                    {
 01403                        '/' => Delimiter,
 01404                        '\\' => '|',
 01405                        _ => c
 01406                    };
 1407                }
 1408
 01409                image.BlurHash = new string(blurHashSpan);
 1410            }
 1411        }
 1412
 01413        return image;
 1414    }
 1415
 1416    private class TrackedMigrationStep : IDisposable
 1417    {
 1418        private readonly string _operationName;
 1419        private readonly ILogger _logger;
 1420        private readonly Stopwatch _operationTimer;
 1421        private bool _disposed;
 1422
 1423        public TrackedMigrationStep(string operationName, ILogger logger)
 1424        {
 01425            _operationName = operationName;
 01426            _logger = logger;
 01427            _operationTimer = Stopwatch.StartNew();
 01428            logger.LogInformation("Start {OperationName}", operationName);
 01429        }
 1430
 1431        public bool Disposed
 1432        {
 01433            get => _disposed;
 01434            set => _disposed = value;
 1435        }
 1436
 1437        public virtual void Dispose()
 1438        {
 01439            if (Disposed)
 1440            {
 01441                return;
 1442            }
 1443
 01444            Disposed = true;
 01445            _logger.LogInformation("{OperationName} took '{Time}'", _operationName, _operationTimer.Elapsed);
 01446        }
 1447    }
 1448
 1449    private sealed class DatabaseMigrationStep : TrackedMigrationStep
 1450    {
 01451        public DatabaseMigrationStep(JellyfinDbContext jellyfinDbContext, string operationName, ILogger logger) : base(o
 1452        {
 1453            JellyfinDbContext = jellyfinDbContext;
 01454        }
 1455
 1456        public JellyfinDbContext JellyfinDbContext { get; }
 1457
 1458        public override void Dispose()
 1459        {
 01460            if (Disposed)
 1461            {
 01462                return;
 1463            }
 1464
 01465            JellyfinDbContext.Dispose();
 01466            base.Dispose();
 01467        }
 1468    }
 1469}