| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469 | 
							- #pragma warning disable RS0030 // Do not use banned APIs
 
- using System;
 
- using System.Collections.Generic;
 
- using System.Collections.Immutable;
 
- using System.Data;
 
- using System.Diagnostics;
 
- using System.Globalization;
 
- using System.IO;
 
- using System.Linq;
 
- using System.Text;
 
- using Emby.Server.Implementations.Data;
 
- using Jellyfin.Database.Implementations;
 
- using Jellyfin.Database.Implementations.Entities;
 
- using Jellyfin.Extensions;
 
- using Jellyfin.Server.Implementations.Item;
 
- using Jellyfin.Server.ServerSetupApp;
 
- using MediaBrowser.Controller;
 
- using MediaBrowser.Controller.Entities;
 
- using MediaBrowser.Model.Entities;
 
- using Microsoft.Data.Sqlite;
 
- using Microsoft.EntityFrameworkCore;
 
- using Microsoft.Extensions.Logging;
 
- using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity;
 
- using Chapter = Jellyfin.Database.Implementations.Entities.Chapter;
 
- namespace Jellyfin.Server.Migrations.Routines;
 
- /// <summary>
 
- /// The migration routine for migrating the userdata database to EF Core.
 
- /// </summary>
 
- [JellyfinMigration("2025-04-20T20:00:00", nameof(MigrateLibraryDb))]
 
- [JellyfinMigrationBackup(JellyfinDb = true, LegacyLibraryDb = true)]
 
- internal class MigrateLibraryDb : IDatabaseMigrationRoutine
 
- {
 
-     private const string DbFilename = "library.db";
 
-     private readonly IStartupLogger _logger;
 
-     private readonly IServerApplicationPaths _paths;
 
-     private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider;
 
-     private readonly IDbContextFactory<JellyfinDbContext> _provider;
 
-     /// <summary>
 
-     /// Initializes a new instance of the <see cref="MigrateLibraryDb"/> class.
 
-     /// </summary>
 
-     /// <param name="startupLogger">The startup logger for Startup UI intigration.</param>
 
-     /// <param name="provider">The database provider.</param>
 
-     /// <param name="paths">The server application paths.</param>
 
-     /// <param name="jellyfinDatabaseProvider">The database provider for special access.</param>
 
-     public MigrateLibraryDb(
 
-         IStartupLogger<MigrateLibraryDb> startupLogger,
 
-         IDbContextFactory<JellyfinDbContext> provider,
 
-         IServerApplicationPaths paths,
 
-         IJellyfinDatabaseProvider jellyfinDatabaseProvider)
 
-     {
 
-         _logger = startupLogger;
 
-         _provider = provider;
 
-         _paths = paths;
 
-         _jellyfinDatabaseProvider = jellyfinDatabaseProvider;
 
-     }
 
-     /// <inheritdoc/>
 
-     public void Perform()
 
-     {
 
-         _logger.LogInformation("Migrating the userdata from library.db may take a while, do not stop Jellyfin.");
 
-         var dataPath = _paths.DataPath;
 
-         var libraryDbPath = Path.Combine(dataPath, DbFilename);
 
-         if (!File.Exists(libraryDbPath))
 
-         {
 
-             _logger.LogError("Cannot migrate {LibraryDb} as it does not exist..", libraryDbPath);
 
-             return;
 
-         }
 
-         using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly");
 
-         var fullOperationTimer = new Stopwatch();
 
-         fullOperationTimer.Start();
 
-         using (var operation = GetPreparedDbContext("Cleanup database"))
 
-         {
 
-             operation.JellyfinDbContext.AttachmentStreamInfos.ExecuteDelete();
 
-             operation.JellyfinDbContext.BaseItems.ExecuteDelete();
 
-             operation.JellyfinDbContext.ItemValues.ExecuteDelete();
 
-             operation.JellyfinDbContext.UserData.ExecuteDelete();
 
-             operation.JellyfinDbContext.MediaStreamInfos.ExecuteDelete();
 
-             operation.JellyfinDbContext.Peoples.ExecuteDelete();
 
-             operation.JellyfinDbContext.PeopleBaseItemMap.ExecuteDelete();
 
-             operation.JellyfinDbContext.Chapters.ExecuteDelete();
 
-             operation.JellyfinDbContext.AncestorIds.ExecuteDelete();
 
-         }
 
-         // notify the other migration to just silently abort because the fix has been applied here already.
 
-         ReseedFolderFlag.RerunGuardFlag = true;
 
-         var legacyBaseItemWithUserKeys = new Dictionary<string, BaseItemEntity>();
 
-         connection.Open();
 
-         var baseItemIds = new HashSet<Guid>();
 
-         using (var operation = GetPreparedDbContext("Moving TypedBaseItem"))
 
-         {
 
-             IDictionary<Guid, (BaseItemEntity BaseItem, string[] Keys)> allItemsLookup = new Dictionary<Guid, (BaseItemEntity BaseItem, string[] Keys)>();
 
-             const string typedBaseItemsQuery =
 
-             """
 
-             SELECT guid, type, data, StartDate, EndDate, ChannelId, IsMovie,
 
-             IsSeries, EpisodeTitle, IsRepeat, CommunityRating, CustomRating, IndexNumber, IsLocked, PreferredMetadataLanguage,
 
-             PreferredMetadataCountryCode, Width, Height, DateLastRefreshed, Name, Path, PremiereDate, Overview, ParentIndexNumber,
 
-             ProductionYear, OfficialRating, ForcedSortName, RunTimeTicks, Size, DateCreated, DateModified, Genres, ParentId, TopParentId,
 
-             Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId,
 
-             DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, SeasonName, SeasonId, SeriesId,
 
-             PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate,
 
-             ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName, CleanName, UnratedType, IsFolder FROM TypedBaseItems
 
-             """;
 
-             using (new TrackedMigrationStep("Loading TypedBaseItems", _logger))
 
-             {
 
-                 foreach (SqliteDataReader dto in connection.Query(typedBaseItemsQuery))
 
-                 {
 
-                     var baseItem = GetItem(dto);
 
-                     allItemsLookup.Add(baseItem.BaseItem.Id, baseItem);
 
-                 }
 
-             }
 
-             bool DoesResolve(Guid? parentId, HashSet<(BaseItemEntity BaseItem, string[] Keys)> checkStack)
 
-             {
 
-                 if (parentId is null)
 
-                 {
 
-                     return true;
 
-                 }
 
-                 if (!allItemsLookup.TryGetValue(parentId.Value, out var parent))
 
-                 {
 
-                     return false; // item is detached and has no root anymore.
 
-                 }
 
-                 if (!checkStack.Add(parent))
 
-                 {
 
-                     return false; // recursive structure. Abort.
 
-                 }
 
-                 return DoesResolve(parent.BaseItem.ParentId, checkStack);
 
-             }
 
-             using (new TrackedMigrationStep("Clean TypedBaseItems hierarchy", _logger))
 
-             {
 
-                 var checkStack = new HashSet<(BaseItemEntity BaseItem, string[] Keys)>();
 
-                 foreach (var item in allItemsLookup)
 
-                 {
 
-                     var cachedItem = item.Value;
 
-                     if (DoesResolve(cachedItem.BaseItem.ParentId, checkStack))
 
-                     {
 
-                         checkStack.Add(cachedItem);
 
-                         operation.JellyfinDbContext.BaseItems.Add(cachedItem.BaseItem);
 
-                         baseItemIds.Add(cachedItem.BaseItem.Id);
 
-                         foreach (var dataKey in cachedItem.Keys)
 
-                         {
 
-                             legacyBaseItemWithUserKeys[dataKey] = cachedItem.BaseItem;
 
-                         }
 
-                     }
 
-                     checkStack.Clear();
 
-                 }
 
-             }
 
-             using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.BaseItems.Local.Count} BaseItem entries", _logger))
 
-             {
 
-                 operation.JellyfinDbContext.SaveChanges();
 
-             }
 
-             allItemsLookup.Clear();
 
-         }
 
-         using (var operation = GetPreparedDbContext("Moving ItemValues"))
 
-         {
 
-             // do not migrate inherited types as they are now properly mapped in search and lookup.
 
-             const string itemValueQuery =
 
-             """
 
-             SELECT ItemId, Type, Value, CleanValue FROM ItemValues
 
-                         WHERE Type <> 6 AND EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = ItemValues.ItemId)
 
-             """;
 
-             // EFCores local lookup sucks. We cannot use context.ItemValues.Local here because its just super slow.
 
-             var localItems = new Dictionary<(int Type, string Value), (Database.Implementations.Entities.ItemValue ItemValue, List<Guid> ItemIds)>();
 
-             using (new TrackedMigrationStep("Loading ItemValues", _logger))
 
-             {
 
-                 foreach (SqliteDataReader dto in connection.Query(itemValueQuery))
 
-                 {
 
-                     var itemId = dto.GetGuid(0);
 
-                     if (!baseItemIds.Contains(itemId))
 
-                     {
 
-                         continue;
 
-                     }
 
-                     var entity = GetItemValue(dto);
 
-                     var key = ((int)entity.Type, entity.Value);
 
-                     if (!localItems.TryGetValue(key, out var existing))
 
-                     {
 
-                         localItems[key] = existing = (entity, []);
 
-                     }
 
-                     existing.ItemIds.Add(itemId);
 
-                 }
 
-                 foreach (var item in localItems)
 
-                 {
 
-                     operation.JellyfinDbContext.ItemValues.Add(item.Value.ItemValue);
 
-                     operation.JellyfinDbContext.ItemValuesMap.AddRange(item.Value.ItemIds.Distinct().Select(f => new ItemValueMap()
 
-                     {
 
-                         Item = null!,
 
-                         ItemValue = null!,
 
-                         ItemId = f,
 
-                         ItemValueId = item.Value.ItemValue.ItemValueId
 
-                     }));
 
-                 }
 
-             }
 
-             using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.ItemValues.Local.Count} ItemValues entries", _logger))
 
-             {
 
-                 operation.JellyfinDbContext.SaveChanges();
 
-             }
 
-         }
 
-         using (var operation = GetPreparedDbContext("Moving UserData"))
 
-         {
 
-             var queryResult = connection.Query(
 
-             """
 
-             SELECT key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex FROM UserDatas
 
-             WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.UserDataKey = UserDatas.key)
 
-             """);
 
-             using (new TrackedMigrationStep("Loading UserData", _logger))
 
-             {
 
-                 var users = operation.JellyfinDbContext.Users.AsNoTracking().ToArray();
 
-                 var userIdBlacklist = new HashSet<int>();
 
-                 foreach (var entity in queryResult)
 
-                 {
 
-                     var userData = GetUserData(users, entity, userIdBlacklist, _logger);
 
-                     if (userData is null)
 
-                     {
 
-                         var userDataId = entity.GetString(0);
 
-                         var internalUserId = entity.GetInt32(1);
 
-                         if (!userIdBlacklist.Contains(internalUserId))
 
-                         {
 
-                             _logger.LogError("Was not able to migrate user data with key {0} because its id {InternalId} does not match any existing user.", userDataId, internalUserId);
 
-                             userIdBlacklist.Add(internalUserId);
 
-                         }
 
-                         continue;
 
-                     }
 
-                     if (!legacyBaseItemWithUserKeys.TryGetValue(userData.CustomDataKey!, out var refItem))
 
-                     {
 
-                         _logger.LogError("Was not able to migrate user data with key {0} because it does not reference a valid BaseItem.", entity.GetString(0));
 
-                         continue;
 
-                     }
 
-                     if (!baseItemIds.Contains(refItem.Id))
 
-                     {
 
-                         continue;
 
-                     }
 
-                     userData.ItemId = refItem.Id;
 
-                     operation.JellyfinDbContext.UserData.Add(userData);
 
-                 }
 
-             }
 
-             legacyBaseItemWithUserKeys.Clear();
 
-             using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.UserData.Local.Count} UserData entries", _logger))
 
-             {
 
-                 operation.JellyfinDbContext.SaveChanges();
 
-             }
 
-         }
 
-         using (var operation = GetPreparedDbContext("Moving MediaStreamInfos"))
 
-         {
 
-             const string mediaStreamQuery =
 
-             """
 
-             SELECT ItemId, StreamIndex, StreamType, Codec, Language, ChannelLayout, Profile, AspectRatio, Path,
 
-             IsInterlaced, BitRate, Channels, SampleRate, IsDefault, IsForced, IsExternal, Height, Width,
 
-             AverageFrameRate, RealFrameRate, Level, PixelFormat, BitDepth, IsAnamorphic, RefFrames, CodecTag,
 
-             Comment, NalLengthSize, IsAvc, Title, TimeBase, CodecTimeBase, ColorPrimaries, ColorSpace, ColorTransfer,
 
-             DvVersionMajor, DvVersionMinor, DvProfile, DvLevel, RpuPresentFlag, ElPresentFlag, BlPresentFlag, DvBlSignalCompatibilityId, IsHearingImpaired
 
-             FROM MediaStreams
 
-             WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = MediaStreams.ItemId)
 
-             """;
 
-             using (new TrackedMigrationStep("Loading MediaStreamInfos", _logger))
 
-             {
 
-                 foreach (SqliteDataReader dto in connection.Query(mediaStreamQuery))
 
-                 {
 
-                     var entity = GetMediaStream(dto);
 
-                     if (!baseItemIds.Contains(entity.ItemId))
 
-                     {
 
-                         continue;
 
-                     }
 
-                     operation.JellyfinDbContext.MediaStreamInfos.Add(entity);
 
-                 }
 
-             }
 
-             using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.MediaStreamInfos.Local.Count} MediaStreamInfos entries", _logger))
 
-             {
 
-                 operation.JellyfinDbContext.SaveChanges();
 
-             }
 
-         }
 
-         using (var operation = GetPreparedDbContext("Moving AttachmentStreamInfos"))
 
-         {
 
-             const string mediaAttachmentQuery =
 
-             """
 
-             SELECT ItemId, AttachmentIndex, Codec, CodecTag, Comment, filename, MIMEType
 
-             FROM mediaattachments
 
-             WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = mediaattachments.ItemId)
 
-             """;
 
-             using (new TrackedMigrationStep("Loading AttachmentStreamInfos", _logger))
 
-             {
 
-                 foreach (SqliteDataReader dto in connection.Query(mediaAttachmentQuery))
 
-                 {
 
-                     var entity = GetMediaAttachment(dto);
 
-                     if (!baseItemIds.Contains(entity.ItemId))
 
-                     {
 
-                         continue;
 
-                     }
 
-                     operation.JellyfinDbContext.AttachmentStreamInfos.Add(entity);
 
-                 }
 
-             }
 
-             using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.AttachmentStreamInfos.Local.Count} AttachmentStreamInfos entries", _logger))
 
-             {
 
-                 operation.JellyfinDbContext.SaveChanges();
 
-             }
 
-         }
 
-         using (var operation = GetPreparedDbContext("Moving People"))
 
-         {
 
-             const string personsQuery =
 
-             """
 
-             SELECT ItemId, Name, Role, PersonType, SortOrder, ListOrder FROM People
 
-             WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = People.ItemId)
 
-             """;
 
-             var peopleCache = new Dictionary<string, (People Person, List<PeopleBaseItemMap> Items)>();
 
-             using (new TrackedMigrationStep("Loading People", _logger))
 
-             {
 
-                 foreach (SqliteDataReader reader in connection.Query(personsQuery))
 
-                 {
 
-                     var itemId = reader.GetGuid(0);
 
-                     if (!baseItemIds.Contains(itemId))
 
-                     {
 
-                         _logger.LogError("Not saving person {0} because it's not in use by any BaseItem", reader.GetString(1));
 
-                         continue;
 
-                     }
 
-                     var entity = GetPerson(reader);
 
-                     if (!peopleCache.TryGetValue(entity.Name + "|" + entity.PersonType, out var personCache))
 
-                     {
 
-                         peopleCache[entity.Name + "|" + entity.PersonType] = personCache = (entity, []);
 
-                     }
 
-                     if (reader.TryGetString(2, out var role))
 
-                     {
 
-                     }
 
-                     int? sortOrder = reader.IsDBNull(4) ? null : reader.GetInt32(4);
 
-                     int? listOrder = reader.IsDBNull(5) ? null : reader.GetInt32(5);
 
-                     personCache.Items.Add(new PeopleBaseItemMap()
 
-                     {
 
-                         Item = null!,
 
-                         ItemId = itemId,
 
-                         People = null!,
 
-                         PeopleId = personCache.Person.Id,
 
-                         ListOrder = listOrder,
 
-                         SortOrder = sortOrder,
 
-                         Role = role
 
-                     });
 
-                 }
 
-                 baseItemIds.Clear();
 
-                 foreach (var item in peopleCache)
 
-                 {
 
-                     operation.JellyfinDbContext.Peoples.Add(item.Value.Person);
 
-                     operation.JellyfinDbContext.PeopleBaseItemMap.AddRange(item.Value.Items.DistinctBy(e => (e.ItemId, e.PeopleId)));
 
-                 }
 
-                 peopleCache.Clear();
 
-             }
 
-             using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.Peoples.Local.Count} People entries and {operation.JellyfinDbContext.PeopleBaseItemMap.Local.Count} maps", _logger))
 
-             {
 
-                 operation.JellyfinDbContext.SaveChanges();
 
-             }
 
-         }
 
-         using (var operation = GetPreparedDbContext("Moving Chapters"))
 
-         {
 
-             const string chapterQuery =
 
-             """
 
-             SELECT ItemId,StartPositionTicks,Name,ImagePath,ImageDateModified,ChapterIndex from Chapters2
 
-             WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = Chapters2.ItemId)
 
-             """;
 
-             using (new TrackedMigrationStep("Loading Chapters", _logger))
 
-             {
 
-                 foreach (SqliteDataReader dto in connection.Query(chapterQuery))
 
-                 {
 
-                     var chapter = GetChapter(dto);
 
-                     if (!baseItemIds.Contains(chapter.ItemId))
 
-                     {
 
-                         continue;
 
-                     }
 
-                     operation.JellyfinDbContext.Chapters.Add(chapter);
 
-                 }
 
-             }
 
-             using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.Chapters.Local.Count} Chapters entries", _logger))
 
-             {
 
-                 operation.JellyfinDbContext.SaveChanges();
 
-             }
 
-         }
 
-         using (var operation = GetPreparedDbContext("Moving AncestorIds"))
 
-         {
 
-             const string ancestorIdsQuery =
 
-             """
 
-             SELECT ItemId, AncestorId, AncestorIdText FROM AncestorIds
 
-             WHERE
 
-             EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.ItemId)
 
-             AND
 
-             EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.AncestorId)
 
-             """;
 
-             using (new TrackedMigrationStep("Loading AncestorIds", _logger))
 
-             {
 
-                 foreach (SqliteDataReader dto in connection.Query(ancestorIdsQuery))
 
-                 {
 
-                     var ancestorId = GetAncestorId(dto);
 
-                     if (!baseItemIds.Contains(ancestorId.ItemId) || !baseItemIds.Contains(ancestorId.ParentItemId))
 
-                     {
 
-                         continue;
 
-                     }
 
-                     operation.JellyfinDbContext.AncestorIds.Add(ancestorId);
 
-                 }
 
-             }
 
-             using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.AncestorIds.Local.Count} AncestorId entries", _logger))
 
-             {
 
-                 operation.JellyfinDbContext.SaveChanges();
 
-             }
 
-         }
 
-         connection.Close();
 
-         _logger.LogInformation("Migration of the Library.db done.");
 
-         _logger.LogInformation("Migrating Library db took {0}.", fullOperationTimer.Elapsed);
 
-         SqliteConnection.ClearAllPools();
 
-         _logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old");
 
-         File.Move(libraryDbPath, libraryDbPath + ".old", true);
 
-     }
 
-     private DatabaseMigrationStep GetPreparedDbContext(string operationName)
 
-     {
 
-         var dbContext = _provider.CreateDbContext();
 
-         dbContext.ChangeTracker.AutoDetectChangesEnabled = false;
 
-         dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
 
-         return new DatabaseMigrationStep(dbContext, operationName, _logger);
 
-     }
 
-     internal static UserData? GetUserData(User[] users, SqliteDataReader dto, HashSet<int> userIdBlacklist, ILogger logger)
 
-     {
 
-         var internalUserId = dto.GetInt32(1);
 
-         if (userIdBlacklist.Contains(internalUserId))
 
-         {
 
-             return null;
 
-         }
 
-         var user = users.FirstOrDefault(e => e.InternalId == internalUserId);
 
-         if (user is null)
 
-         {
 
-             userIdBlacklist.Add(internalUserId);
 
-             logger.LogError("Tried to find user with index '{Idx}' but there are only '{MaxIdx}' users.", internalUserId, users.Length);
 
-             return null;
 
-         }
 
-         var oldKey = dto.GetString(0);
 
-         return new UserData()
 
-         {
 
-             ItemId = Guid.NewGuid(),
 
-             CustomDataKey = oldKey,
 
-             UserId = user.Id,
 
-             Rating = dto.IsDBNull(2) ? null : dto.GetDouble(2),
 
-             Played = dto.GetBoolean(3),
 
-             PlayCount = dto.GetInt32(4),
 
-             IsFavorite = dto.GetBoolean(5),
 
-             PlaybackPositionTicks = dto.GetInt64(6),
 
-             LastPlayedDate = dto.IsDBNull(7) ? null : dto.GetDateTime(7),
 
-             AudioStreamIndex = dto.IsDBNull(8) ? null : dto.GetInt32(8),
 
-             SubtitleStreamIndex = dto.IsDBNull(9) ? null : dto.GetInt32(9),
 
-             Likes = null,
 
-             User = null!,
 
-             Item = null!
 
-         };
 
-     }
 
-     private AncestorId GetAncestorId(SqliteDataReader reader)
 
-     {
 
-         return new AncestorId()
 
-         {
 
-             ItemId = reader.GetGuid(0),
 
-             ParentItemId = reader.GetGuid(1),
 
-             Item = null!,
 
-             ParentItem = null!
 
-         };
 
-     }
 
-     /// <summary>
 
-     /// Gets the chapter.
 
-     /// </summary>
 
-     /// <param name="reader">The reader.</param>
 
-     /// <returns>ChapterInfo.</returns>
 
-     private Chapter GetChapter(SqliteDataReader reader)
 
-     {
 
-         var chapter = new Chapter
 
-         {
 
-             StartPositionTicks = reader.GetInt64(1),
 
-             ChapterIndex = reader.GetInt32(5),
 
-             Item = null!,
 
-             ItemId = reader.GetGuid(0),
 
-         };
 
-         if (reader.TryGetString(2, out var chapterName))
 
-         {
 
-             chapter.Name = chapterName;
 
-         }
 
-         if (reader.TryGetString(3, out var imagePath))
 
-         {
 
-             chapter.ImagePath = imagePath;
 
-         }
 
-         if (reader.TryReadDateTime(4, out var imageDateModified))
 
-         {
 
-             chapter.ImageDateModified = imageDateModified;
 
-         }
 
-         return chapter;
 
-     }
 
-     private ItemValue GetItemValue(SqliteDataReader reader)
 
-     {
 
-         return new ItemValue
 
-         {
 
-             ItemValueId = Guid.NewGuid(),
 
-             Type = (ItemValueType)reader.GetInt32(1),
 
-             Value = reader.GetString(2),
 
-             CleanValue = reader.GetString(3),
 
-         };
 
-     }
 
-     private People GetPerson(SqliteDataReader reader)
 
-     {
 
-         var item = new People
 
-         {
 
-             Id = Guid.NewGuid(),
 
-             Name = reader.GetString(1),
 
-         };
 
-         if (reader.TryGetString(3, out var type))
 
-         {
 
-             item.PersonType = type;
 
-         }
 
-         return item;
 
-     }
 
-     /// <summary>
 
-     /// Gets the media stream.
 
-     /// </summary>
 
-     /// <param name="reader">The reader.</param>
 
-     /// <returns>MediaStream.</returns>
 
-     private MediaStreamInfo GetMediaStream(SqliteDataReader reader)
 
-     {
 
-         var item = new MediaStreamInfo
 
-         {
 
-             StreamIndex = reader.GetInt32(1),
 
-             StreamType = Enum.Parse<MediaStreamTypeEntity>(reader.GetString(2)),
 
-             Item = null!,
 
-             ItemId = reader.GetGuid(0),
 
-             AspectRatio = null!,
 
-             ChannelLayout = null!,
 
-             Codec = null!,
 
-             IsInterlaced = false,
 
-             Language = null!,
 
-             Path = null!,
 
-             Profile = null!,
 
-         };
 
-         if (reader.TryGetString(3, out var codec))
 
-         {
 
-             item.Codec = codec;
 
-         }
 
-         if (reader.TryGetString(4, out var language))
 
-         {
 
-             item.Language = language;
 
-         }
 
-         if (reader.TryGetString(5, out var channelLayout))
 
-         {
 
-             item.ChannelLayout = channelLayout;
 
-         }
 
-         if (reader.TryGetString(6, out var profile))
 
-         {
 
-             item.Profile = profile;
 
-         }
 
-         if (reader.TryGetString(7, out var aspectRatio))
 
-         {
 
-             item.AspectRatio = aspectRatio;
 
-         }
 
-         if (reader.TryGetString(8, out var path))
 
-         {
 
-             item.Path = path;
 
-         }
 
-         item.IsInterlaced = reader.GetBoolean(9);
 
-         if (reader.TryGetInt32(10, out var bitrate))
 
-         {
 
-             item.BitRate = bitrate;
 
-         }
 
-         if (reader.TryGetInt32(11, out var channels))
 
-         {
 
-             item.Channels = channels;
 
-         }
 
-         if (reader.TryGetInt32(12, out var sampleRate))
 
-         {
 
-             item.SampleRate = sampleRate;
 
-         }
 
-         item.IsDefault = reader.GetBoolean(13);
 
-         item.IsForced = reader.GetBoolean(14);
 
-         item.IsExternal = reader.GetBoolean(15);
 
-         if (reader.TryGetInt32(16, out var width))
 
-         {
 
-             item.Width = width;
 
-         }
 
-         if (reader.TryGetInt32(17, out var height))
 
-         {
 
-             item.Height = height;
 
-         }
 
-         if (reader.TryGetSingle(18, out var averageFrameRate))
 
-         {
 
-             item.AverageFrameRate = averageFrameRate;
 
-         }
 
-         if (reader.TryGetSingle(19, out var realFrameRate))
 
-         {
 
-             item.RealFrameRate = realFrameRate;
 
-         }
 
-         if (reader.TryGetSingle(20, out var level))
 
-         {
 
-             item.Level = level;
 
-         }
 
-         if (reader.TryGetString(21, out var pixelFormat))
 
-         {
 
-             item.PixelFormat = pixelFormat;
 
-         }
 
-         if (reader.TryGetInt32(22, out var bitDepth))
 
-         {
 
-             item.BitDepth = bitDepth;
 
-         }
 
-         if (reader.TryGetBoolean(23, out var isAnamorphic))
 
-         {
 
-             item.IsAnamorphic = isAnamorphic;
 
-         }
 
-         if (reader.TryGetInt32(24, out var refFrames))
 
-         {
 
-             item.RefFrames = refFrames;
 
-         }
 
-         if (reader.TryGetString(25, out var codecTag))
 
-         {
 
-             item.CodecTag = codecTag;
 
-         }
 
-         if (reader.TryGetString(26, out var comment))
 
-         {
 
-             item.Comment = comment;
 
-         }
 
-         if (reader.TryGetString(27, out var nalLengthSize))
 
-         {
 
-             item.NalLengthSize = nalLengthSize;
 
-         }
 
-         if (reader.TryGetBoolean(28, out var isAVC))
 
-         {
 
-             item.IsAvc = isAVC;
 
-         }
 
-         if (reader.TryGetString(29, out var title))
 
-         {
 
-             item.Title = title;
 
-         }
 
-         if (reader.TryGetString(30, out var timeBase))
 
-         {
 
-             item.TimeBase = timeBase;
 
-         }
 
-         if (reader.TryGetString(31, out var codecTimeBase))
 
-         {
 
-             item.CodecTimeBase = codecTimeBase;
 
-         }
 
-         if (reader.TryGetString(32, out var colorPrimaries))
 
-         {
 
-             item.ColorPrimaries = colorPrimaries;
 
-         }
 
-         if (reader.TryGetString(33, out var colorSpace))
 
-         {
 
-             item.ColorSpace = colorSpace;
 
-         }
 
-         if (reader.TryGetString(34, out var colorTransfer))
 
-         {
 
-             item.ColorTransfer = colorTransfer;
 
-         }
 
-         if (reader.TryGetInt32(35, out var dvVersionMajor))
 
-         {
 
-             item.DvVersionMajor = dvVersionMajor;
 
-         }
 
-         if (reader.TryGetInt32(36, out var dvVersionMinor))
 
-         {
 
-             item.DvVersionMinor = dvVersionMinor;
 
-         }
 
-         if (reader.TryGetInt32(37, out var dvProfile))
 
-         {
 
-             item.DvProfile = dvProfile;
 
-         }
 
-         if (reader.TryGetInt32(38, out var dvLevel))
 
-         {
 
-             item.DvLevel = dvLevel;
 
-         }
 
-         if (reader.TryGetInt32(39, out var rpuPresentFlag))
 
-         {
 
-             item.RpuPresentFlag = rpuPresentFlag;
 
-         }
 
-         if (reader.TryGetInt32(40, out var elPresentFlag))
 
-         {
 
-             item.ElPresentFlag = elPresentFlag;
 
-         }
 
-         if (reader.TryGetInt32(41, out var blPresentFlag))
 
-         {
 
-             item.BlPresentFlag = blPresentFlag;
 
-         }
 
-         if (reader.TryGetInt32(42, out var dvBlSignalCompatibilityId))
 
-         {
 
-             item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId;
 
-         }
 
-         item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result;
 
-         // if (reader.TryGetInt32(44, out var rotation))
 
-         // {
 
-         //     item.Rotation = rotation;
 
-         // }
 
-         return item;
 
-     }
 
-     /// <summary>
 
-     /// Gets the attachment.
 
-     /// </summary>
 
-     /// <param name="reader">The reader.</param>
 
-     /// <returns>MediaAttachment.</returns>
 
-     private AttachmentStreamInfo GetMediaAttachment(SqliteDataReader reader)
 
-     {
 
-         var item = new AttachmentStreamInfo
 
-         {
 
-             Index = reader.GetInt32(1),
 
-             Item = null!,
 
-             ItemId = reader.GetGuid(0),
 
-         };
 
-         if (reader.TryGetString(2, out var codec))
 
-         {
 
-             item.Codec = codec;
 
-         }
 
-         if (reader.TryGetString(3, out var codecTag))
 
-         {
 
-             item.CodecTag = codecTag;
 
-         }
 
-         if (reader.TryGetString(4, out var comment))
 
-         {
 
-             item.Comment = comment;
 
-         }
 
-         if (reader.TryGetString(5, out var fileName))
 
-         {
 
-             item.Filename = fileName;
 
-         }
 
-         if (reader.TryGetString(6, out var mimeType))
 
-         {
 
-             item.MimeType = mimeType;
 
-         }
 
-         return item;
 
-     }
 
-     private (BaseItemEntity BaseItem, string[] LegacyUserDataKey) GetItem(SqliteDataReader reader)
 
-     {
 
-         var entity = new BaseItemEntity()
 
-         {
 
-             Id = reader.GetGuid(0),
 
-             Type = reader.GetString(1),
 
-         };
 
-         var index = 2;
 
-         if (reader.TryGetString(index++, out var data))
 
-         {
 
-             entity.Data = data;
 
-         }
 
-         if (reader.TryReadDateTime(index++, out var startDate))
 
-         {
 
-             entity.StartDate = startDate;
 
-         }
 
-         if (reader.TryReadDateTime(index++, out var endDate))
 
-         {
 
-             entity.EndDate = endDate;
 
-         }
 
-         if (reader.TryGetGuid(index++, out var guid))
 
-         {
 
-             entity.ChannelId = guid;
 
-         }
 
-         if (reader.TryGetBoolean(index++, out var isMovie))
 
-         {
 
-             entity.IsMovie = isMovie;
 
-         }
 
-         if (reader.TryGetBoolean(index++, out var isSeries))
 
-         {
 
-             entity.IsSeries = isSeries;
 
-         }
 
-         if (reader.TryGetString(index++, out var episodeTitle))
 
-         {
 
-             entity.EpisodeTitle = episodeTitle;
 
-         }
 
-         if (reader.TryGetBoolean(index++, out var isRepeat))
 
-         {
 
-             entity.IsRepeat = isRepeat;
 
-         }
 
-         if (reader.TryGetSingle(index++, out var communityRating))
 
-         {
 
-             entity.CommunityRating = communityRating;
 
-         }
 
-         if (reader.TryGetString(index++, out var customRating))
 
-         {
 
-             entity.CustomRating = customRating;
 
-         }
 
-         if (reader.TryGetInt32(index++, out var indexNumber))
 
-         {
 
-             entity.IndexNumber = indexNumber;
 
-         }
 
-         if (reader.TryGetBoolean(index++, out var isLocked))
 
-         {
 
-             entity.IsLocked = isLocked;
 
-         }
 
-         if (reader.TryGetString(index++, out var preferredMetadataLanguage))
 
-         {
 
-             entity.PreferredMetadataLanguage = preferredMetadataLanguage;
 
-         }
 
-         if (reader.TryGetString(index++, out var preferredMetadataCountryCode))
 
-         {
 
-             entity.PreferredMetadataCountryCode = preferredMetadataCountryCode;
 
-         }
 
-         if (reader.TryGetInt32(index++, out var width))
 
-         {
 
-             entity.Width = width;
 
-         }
 
-         if (reader.TryGetInt32(index++, out var height))
 
-         {
 
-             entity.Height = height;
 
-         }
 
-         if (reader.TryReadDateTime(index++, out var dateLastRefreshed))
 
-         {
 
-             entity.DateLastRefreshed = dateLastRefreshed;
 
-         }
 
-         if (reader.TryGetString(index++, out var name))
 
-         {
 
-             entity.Name = name;
 
-         }
 
-         if (reader.TryGetString(index++, out var restorePath))
 
-         {
 
-             entity.Path = restorePath;
 
-         }
 
-         if (reader.TryReadDateTime(index++, out var premiereDate))
 
-         {
 
-             entity.PremiereDate = premiereDate;
 
-         }
 
-         if (reader.TryGetString(index++, out var overview))
 
-         {
 
-             entity.Overview = overview;
 
-         }
 
-         if (reader.TryGetInt32(index++, out var parentIndexNumber))
 
-         {
 
-             entity.ParentIndexNumber = parentIndexNumber;
 
-         }
 
-         if (reader.TryGetInt32(index++, out var productionYear))
 
-         {
 
-             entity.ProductionYear = productionYear;
 
-         }
 
-         if (reader.TryGetString(index++, out var officialRating))
 
-         {
 
-             entity.OfficialRating = officialRating;
 
-         }
 
-         if (reader.TryGetString(index++, out var forcedSortName))
 
-         {
 
-             entity.ForcedSortName = forcedSortName;
 
-         }
 
-         if (reader.TryGetInt64(index++, out var runTimeTicks))
 
-         {
 
-             entity.RunTimeTicks = runTimeTicks;
 
-         }
 
-         if (reader.TryGetInt64(index++, out var size))
 
-         {
 
-             entity.Size = size;
 
-         }
 
-         if (reader.TryReadDateTime(index++, out var dateCreated))
 
-         {
 
-             entity.DateCreated = dateCreated;
 
-         }
 
-         if (reader.TryReadDateTime(index++, out var dateModified))
 
-         {
 
-             entity.DateModified = dateModified;
 
-         }
 
-         if (reader.TryGetString(index++, out var genres))
 
-         {
 
-             entity.Genres = genres;
 
-         }
 
-         if (reader.TryGetGuid(index++, out var parentId))
 
-         {
 
-             entity.ParentId = parentId;
 
-         }
 
-         if (reader.TryGetGuid(index++, out var topParentId))
 
-         {
 
-             entity.TopParentId = topParentId;
 
-         }
 
-         if (reader.TryGetString(index++, out var audioString) && Enum.TryParse<ProgramAudioEntity>(audioString, out var audioType))
 
-         {
 
-             entity.Audio = audioType;
 
-         }
 
-         if (reader.TryGetString(index++, out var serviceName))
 
-         {
 
-             entity.ExternalServiceId = serviceName;
 
-         }
 
-         if (reader.TryGetBoolean(index++, out var isInMixedFolder))
 
-         {
 
-             entity.IsInMixedFolder = isInMixedFolder;
 
-         }
 
-         if (reader.TryReadDateTime(index++, out var dateLastSaved))
 
-         {
 
-             entity.DateLastSaved = dateLastSaved;
 
-         }
 
-         if (reader.TryGetString(index++, out var lockedFields))
 
-         {
 
-             entity.LockedFields = lockedFields.Split('|').Select(Enum.Parse<MetadataField>)
 
-                 .Select(e => new BaseItemMetadataField()
 
-                 {
 
-                     Id = (int)e,
 
-                     Item = entity,
 
-                     ItemId = entity.Id
 
-                 })
 
-                 .ToArray();
 
-         }
 
-         if (reader.TryGetString(index++, out var studios))
 
-         {
 
-             entity.Studios = studios;
 
-         }
 
-         if (reader.TryGetString(index++, out var tags))
 
-         {
 
-             entity.Tags = tags;
 
-         }
 
-         if (reader.TryGetString(index++, out var trailerTypes))
 
-         {
 
-             entity.TrailerTypes = trailerTypes.Split('|').Select(Enum.Parse<TrailerType>)
 
-                 .Select(e => new BaseItemTrailerType()
 
-                 {
 
-                     Id = (int)e,
 
-                     Item = entity,
 
-                     ItemId = entity.Id
 
-                 })
 
-                 .ToArray();
 
-         }
 
-         if (reader.TryGetString(index++, out var originalTitle))
 
-         {
 
-             entity.OriginalTitle = originalTitle;
 
-         }
 
-         if (reader.TryGetString(index++, out var primaryVersionId))
 
-         {
 
-             entity.PrimaryVersionId = primaryVersionId;
 
-         }
 
-         if (reader.TryReadDateTime(index++, out var dateLastMediaAdded))
 
-         {
 
-             entity.DateLastMediaAdded = dateLastMediaAdded;
 
-         }
 
-         if (reader.TryGetString(index++, out var album))
 
-         {
 
-             entity.Album = album;
 
-         }
 
-         if (reader.TryGetSingle(index++, out var lUFS))
 
-         {
 
-             entity.LUFS = lUFS;
 
-         }
 
-         if (reader.TryGetSingle(index++, out var normalizationGain))
 
-         {
 
-             entity.NormalizationGain = normalizationGain;
 
-         }
 
-         if (reader.TryGetSingle(index++, out var criticRating))
 
-         {
 
-             entity.CriticRating = criticRating;
 
-         }
 
-         if (reader.TryGetBoolean(index++, out var isVirtualItem))
 
-         {
 
-             entity.IsVirtualItem = isVirtualItem;
 
-         }
 
-         if (reader.TryGetString(index++, out var seriesName))
 
-         {
 
-             entity.SeriesName = seriesName;
 
-         }
 
-         var userDataKeys = new List<string>();
 
-         if (reader.TryGetString(index++, out var directUserDataKey))
 
-         {
 
-             userDataKeys.Add(directUserDataKey);
 
-         }
 
-         if (reader.TryGetString(index++, out var seasonName))
 
-         {
 
-             entity.SeasonName = seasonName;
 
-         }
 
-         if (reader.TryGetGuid(index++, out var seasonId))
 
-         {
 
-             entity.SeasonId = seasonId;
 
-         }
 
-         if (reader.TryGetGuid(index++, out var seriesId))
 
-         {
 
-             entity.SeriesId = seriesId;
 
-         }
 
-         if (reader.TryGetString(index++, out var presentationUniqueKey))
 
-         {
 
-             entity.PresentationUniqueKey = presentationUniqueKey;
 
-         }
 
-         if (reader.TryGetInt32(index++, out var parentalRating))
 
-         {
 
-             entity.InheritedParentalRatingValue = parentalRating;
 
-         }
 
-         if (reader.TryGetString(index++, out var externalSeriesId))
 
-         {
 
-             entity.ExternalSeriesId = externalSeriesId;
 
-         }
 
-         if (reader.TryGetString(index++, out var tagLine))
 
-         {
 
-             entity.Tagline = tagLine;
 
-         }
 
-         if (reader.TryGetString(index++, out var providerIds))
 
-         {
 
-             entity.Provider = providerIds.Split('|').Select(e => e.Split("=")).Where(e => e.Length >= 2)
 
-             .Select(e => new BaseItemProvider()
 
-             {
 
-                 Item = null!,
 
-                 ProviderId = e[0],
 
-                 ProviderValue = string.Join('|', e.Skip(1))
 
-             }).ToArray();
 
-         }
 
-         if (reader.TryGetString(index++, out var imageInfos))
 
-         {
 
-             entity.Images = DeserializeImages(imageInfos).Select(f => Map(entity.Id, f)).ToArray();
 
-         }
 
-         if (reader.TryGetString(index++, out var productionLocations))
 
-         {
 
-             entity.ProductionLocations = productionLocations;
 
-         }
 
-         if (reader.TryGetString(index++, out var extraIds))
 
-         {
 
-             entity.ExtraIds = extraIds;
 
-         }
 
-         if (reader.TryGetInt32(index++, out var totalBitrate))
 
-         {
 
-             entity.TotalBitrate = totalBitrate;
 
-         }
 
-         if (reader.TryGetString(index++, out var extraTypeString) && Enum.TryParse<BaseItemExtraType>(extraTypeString, out var extraType))
 
-         {
 
-             entity.ExtraType = extraType;
 
-         }
 
-         if (reader.TryGetString(index++, out var artists))
 
-         {
 
-             entity.Artists = artists;
 
-         }
 
-         if (reader.TryGetString(index++, out var albumArtists))
 
-         {
 
-             entity.AlbumArtists = albumArtists;
 
-         }
 
-         if (reader.TryGetString(index++, out var externalId))
 
-         {
 
-             entity.ExternalId = externalId;
 
-         }
 
-         if (reader.TryGetString(index++, out var seriesPresentationUniqueKey))
 
-         {
 
-             entity.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
 
-         }
 
-         if (reader.TryGetString(index++, out var showId))
 
-         {
 
-             entity.ShowId = showId;
 
-         }
 
-         if (reader.TryGetString(index++, out var ownerId))
 
-         {
 
-             entity.OwnerId = ownerId;
 
-         }
 
-         if (reader.TryGetString(index++, out var mediaType))
 
-         {
 
-             entity.MediaType = mediaType;
 
-         }
 
-         if (reader.TryGetString(index++, out var sortName))
 
-         {
 
-             entity.SortName = sortName;
 
-         }
 
-         if (reader.TryGetString(index++, out var cleanName))
 
-         {
 
-             entity.CleanName = cleanName;
 
-         }
 
-         if (reader.TryGetString(index++, out var unratedType))
 
-         {
 
-             entity.UnratedType = unratedType;
 
-         }
 
-         if (reader.TryGetBoolean(index++, out var isFolder))
 
-         {
 
-             entity.IsFolder = isFolder;
 
-         }
 
-         var baseItem = BaseItemRepository.DeserializeBaseItem(entity, _logger, null, false);
 
-         var dataKeys = baseItem.GetUserDataKeys();
 
-         userDataKeys.AddRange(dataKeys);
 
-         return (entity, userDataKeys.ToArray());
 
-     }
 
-     private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
 
-     {
 
-         return new BaseItemImageInfo()
 
-         {
 
-             ItemId = baseItemId,
 
-             Id = Guid.NewGuid(),
 
-             Path = e.Path,
 
-             Blurhash = e.BlurHash is not null ? Encoding.UTF8.GetBytes(e.BlurHash) : null,
 
-             DateModified = e.DateModified,
 
-             Height = e.Height,
 
-             Width = e.Width,
 
-             ImageType = (ImageInfoImageType)e.Type,
 
-             Item = null!
 
-         };
 
-     }
 
-     internal ItemImageInfo[] DeserializeImages(string value)
 
-     {
 
-         if (string.IsNullOrWhiteSpace(value))
 
-         {
 
-             return Array.Empty<ItemImageInfo>();
 
-         }
 
-         // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the database would be malformed
 
-         var valueSpan = value.AsSpan();
 
-         var count = valueSpan.Count('|') + 1;
 
-         var position = 0;
 
-         var result = new ItemImageInfo[count];
 
-         foreach (var part in valueSpan.Split('|'))
 
-         {
 
-             var image = ItemImageInfoFromValueString(part);
 
-             if (image is not null)
 
-             {
 
-                 result[position++] = image;
 
-             }
 
-         }
 
-         if (position == count)
 
-         {
 
-             return result;
 
-         }
 
-         if (position == 0)
 
-         {
 
-             return Array.Empty<ItemImageInfo>();
 
-         }
 
-         // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array.
 
-         return result[..position];
 
-     }
 
-     internal ItemImageInfo? ItemImageInfoFromValueString(ReadOnlySpan<char> value)
 
-     {
 
-         const char Delimiter = '*';
 
-         var nextSegment = value.IndexOf(Delimiter);
 
-         if (nextSegment == -1)
 
-         {
 
-             return null;
 
-         }
 
-         ReadOnlySpan<char> path = value[..nextSegment];
 
-         value = value[(nextSegment + 1)..];
 
-         nextSegment = value.IndexOf(Delimiter);
 
-         if (nextSegment == -1)
 
-         {
 
-             return null;
 
-         }
 
-         ReadOnlySpan<char> dateModified = value[..nextSegment];
 
-         value = value[(nextSegment + 1)..];
 
-         nextSegment = value.IndexOf(Delimiter);
 
-         if (nextSegment == -1)
 
-         {
 
-             nextSegment = value.Length;
 
-         }
 
-         ReadOnlySpan<char> imageType = value[..nextSegment];
 
-         var image = new ItemImageInfo
 
-         {
 
-             Path = path.ToString()
 
-         };
 
-         if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks)
 
-             && ticks >= DateTime.MinValue.Ticks
 
-             && ticks <= DateTime.MaxValue.Ticks)
 
-         {
 
-             image.DateModified = new DateTime(ticks, DateTimeKind.Utc);
 
-         }
 
-         else
 
-         {
 
-             return null;
 
-         }
 
-         if (Enum.TryParse(imageType, true, out ImageType type))
 
-         {
 
-             image.Type = type;
 
-         }
 
-         else
 
-         {
 
-             return null;
 
-         }
 
-         // Optional parameters: width*height*blurhash
 
-         if (nextSegment + 1 < value.Length - 1)
 
-         {
 
-             value = value[(nextSegment + 1)..];
 
-             nextSegment = value.IndexOf(Delimiter);
 
-             if (nextSegment == -1 || nextSegment == value.Length)
 
-             {
 
-                 return image;
 
-             }
 
-             ReadOnlySpan<char> widthSpan = value[..nextSegment];
 
-             value = value[(nextSegment + 1)..];
 
-             nextSegment = value.IndexOf(Delimiter);
 
-             if (nextSegment == -1)
 
-             {
 
-                 nextSegment = value.Length;
 
-             }
 
-             ReadOnlySpan<char> heightSpan = value[..nextSegment];
 
-             if (int.TryParse(widthSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var width)
 
-                 && int.TryParse(heightSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var height))
 
-             {
 
-                 image.Width = width;
 
-                 image.Height = height;
 
-             }
 
-             if (nextSegment < value.Length - 1)
 
-             {
 
-                 value = value[(nextSegment + 1)..];
 
-                 var length = value.Length;
 
-                 Span<char> blurHashSpan = stackalloc char[length];
 
-                 for (int i = 0; i < length; i++)
 
-                 {
 
-                     var c = value[i];
 
-                     blurHashSpan[i] = c switch
 
-                     {
 
-                         '/' => Delimiter,
 
-                         '\\' => '|',
 
-                         _ => c
 
-                     };
 
-                 }
 
-                 image.BlurHash = new string(blurHashSpan);
 
-             }
 
-         }
 
-         return image;
 
-     }
 
-     private class TrackedMigrationStep : IDisposable
 
-     {
 
-         private readonly string _operationName;
 
-         private readonly ILogger _logger;
 
-         private readonly Stopwatch _operationTimer;
 
-         private bool _disposed;
 
-         public TrackedMigrationStep(string operationName, ILogger logger)
 
-         {
 
-             _operationName = operationName;
 
-             _logger = logger;
 
-             _operationTimer = Stopwatch.StartNew();
 
-             logger.LogInformation("Start {OperationName}", operationName);
 
-         }
 
-         public bool Disposed
 
-         {
 
-             get => _disposed;
 
-             set => _disposed = value;
 
-         }
 
-         public virtual void Dispose()
 
-         {
 
-             if (Disposed)
 
-             {
 
-                 return;
 
-             }
 
-             Disposed = true;
 
-             _logger.LogInformation("{OperationName} took '{Time}'", _operationName, _operationTimer.Elapsed);
 
-         }
 
-     }
 
-     private sealed class DatabaseMigrationStep : TrackedMigrationStep
 
-     {
 
-         public DatabaseMigrationStep(JellyfinDbContext jellyfinDbContext, string operationName, ILogger logger) : base(operationName, logger)
 
-         {
 
-             JellyfinDbContext = jellyfinDbContext;
 
-         }
 
-         public JellyfinDbContext JellyfinDbContext { get; }
 
-         public override void Dispose()
 
-         {
 
-             if (Disposed)
 
-             {
 
-                 return;
 
-             }
 
-             JellyfinDbContext.Dispose();
 
-             base.Dispose();
 
-         }
 
-     }
 
- }
 
 
  |