123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212 |
- #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 System.Threading;
- using Emby.Server.Implementations.Data;
- using Jellyfin.Data.Entities;
- using Jellyfin.Database.Implementations;
- using Jellyfin.Extensions;
- using Jellyfin.Server.Implementations.Item;
- using MediaBrowser.Controller;
- using MediaBrowser.Controller.Entities;
- using MediaBrowser.Model.Entities;
- using Microsoft.Data.Sqlite;
- using Microsoft.EntityFrameworkCore;
- using Microsoft.Extensions.Logging;
- using Chapter = Jellyfin.Data.Entities.Chapter;
- namespace Jellyfin.Server.Migrations.Routines;
- /// <summary>
- /// The migration routine for migrating the userdata database to EF Core.
- /// </summary>
- public class MigrateLibraryDb : IMigrationRoutine
- {
- private const string DbFilename = "library.db";
- private readonly ILogger<MigrateLibraryDb> _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="logger">The logger.</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(
- ILogger<MigrateLibraryDb> logger,
- IDbContextFactory<JellyfinDbContext> provider,
- IServerApplicationPaths paths,
- IJellyfinDatabaseProvider jellyfinDatabaseProvider)
- {
- _logger = logger;
- _provider = provider;
- _paths = paths;
- _jellyfinDatabaseProvider = jellyfinDatabaseProvider;
- }
- /// <inheritdoc/>
- public Guid Id => Guid.Parse("36445464-849f-429f-9ad0-bb130efa0664");
- /// <inheritdoc/>
- public string Name => "MigrateLibraryDbData";
- /// <inheritdoc/>
- public bool PerformOnNewInstall => false; // TODO Change back after testing
- /// <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);
- using var connection = new SqliteConnection($"Filename={libraryDbPath}");
- var migrationTotalTime = TimeSpan.Zero;
- var stopwatch = new Stopwatch();
- stopwatch.Start();
- connection.Open();
- using var dbContext = _provider.CreateDbContext();
- migrationTotalTime += stopwatch.Elapsed;
- _logger.LogInformation("Saving UserData entries took {0}.", stopwatch.Elapsed);
- stopwatch.Restart();
- _logger.LogInformation("Start moving TypedBaseItem.");
- 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 FROM TypedBaseItems
- """;
- dbContext.BaseItems.ExecuteDelete();
- var legacyBaseItemWithUserKeys = new Dictionary<string, BaseItemEntity>();
- foreach (SqliteDataReader dto in connection.Query(typedBaseItemsQuery))
- {
- var baseItem = GetItem(dto);
- dbContext.BaseItems.Add(baseItem.BaseItem);
- foreach (var dataKey in baseItem.LegacyUserDataKey)
- {
- legacyBaseItemWithUserKeys[dataKey] = baseItem.BaseItem;
- }
- }
- _logger.LogInformation("Try saving {0} BaseItem entries.", dbContext.BaseItems.Local.Count);
- dbContext.SaveChanges();
- migrationTotalTime += stopwatch.Elapsed;
- _logger.LogInformation("Saving BaseItems entries took {0}.", stopwatch.Elapsed);
- stopwatch.Restart();
- _logger.LogInformation("Start 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)
- """;
- dbContext.ItemValues.ExecuteDelete();
- // EFCores local lookup sucks. We cannot use context.ItemValues.Local here because its just super slow.
- var localItems = new Dictionary<(int Type, string CleanValue), (ItemValue ItemValue, List<Guid> ItemIds)>();
- foreach (SqliteDataReader dto in connection.Query(itemValueQuery))
- {
- var itemId = dto.GetGuid(0);
- var entity = GetItemValue(dto);
- var key = ((int)entity.Type, entity.CleanValue);
- if (!localItems.TryGetValue(key, out var existing))
- {
- localItems[key] = existing = (entity, []);
- }
- existing.ItemIds.Add(itemId);
- }
- foreach (var item in localItems)
- {
- dbContext.ItemValues.Add(item.Value.ItemValue);
- dbContext.ItemValuesMap.AddRange(item.Value.ItemIds.Distinct().Select(f => new ItemValueMap()
- {
- Item = null!,
- ItemValue = null!,
- ItemId = f,
- ItemValueId = item.Value.ItemValue.ItemValueId
- }));
- }
- _logger.LogInformation("Try saving {0} ItemValues entries.", dbContext.ItemValues.Local.Count);
- dbContext.SaveChanges();
- migrationTotalTime += stopwatch.Elapsed;
- _logger.LogInformation("Saving People ItemValues took {0}.", stopwatch.Elapsed);
- stopwatch.Restart();
- _logger.LogInformation("Start 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)
- """);
- dbContext.UserData.ExecuteDelete();
- var users = dbContext.Users.AsNoTracking().ToImmutableArray();
- foreach (var entity in queryResult)
- {
- var userData = GetUserData(users, entity);
- if (userData is null)
- {
- _logger.LogError("Was not able to migrate user data with key {0}", entity.GetString(0));
- 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;
- }
- userData.ItemId = refItem.Id;
- dbContext.UserData.Add(userData);
- }
- users.Clear();
- legacyBaseItemWithUserKeys.Clear();
- _logger.LogInformation("Try saving {0} UserData entries.", dbContext.UserData.Local.Count);
- dbContext.SaveChanges();
- _logger.LogInformation("Start 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)
- """;
- dbContext.MediaStreamInfos.ExecuteDelete();
- foreach (SqliteDataReader dto in connection.Query(mediaStreamQuery))
- {
- dbContext.MediaStreamInfos.Add(GetMediaStream(dto));
- }
- _logger.LogInformation("Try saving {0} MediaStreamInfos entries.", dbContext.MediaStreamInfos.Local.Count);
- dbContext.SaveChanges();
- migrationTotalTime += stopwatch.Elapsed;
- _logger.LogInformation("Saving MediaStreamInfos entries took {0}.", stopwatch.Elapsed);
- stopwatch.Restart();
- _logger.LogInformation("Start moving People.");
- const string personsQuery = """
- SELECT ItemId, Name, Role, PersonType, SortOrder FROM People
- WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = People.ItemId)
- """;
- dbContext.Peoples.ExecuteDelete();
- dbContext.PeopleBaseItemMap.ExecuteDelete();
- var peopleCache = new Dictionary<string, (People Person, List<PeopleBaseItemMap> Items)>();
- var baseItemIds = dbContext.BaseItems.Select(b => b.Id).ToHashSet();
- foreach (SqliteDataReader reader in connection.Query(personsQuery))
- {
- var itemId = reader.GetGuid(0);
- if (!baseItemIds.Contains(itemId))
- {
- _logger.LogError("Dont save person {0} because its not in use by any BaseItem", reader.GetString(1));
- continue;
- }
- var entity = GetPerson(reader);
- if (!peopleCache.TryGetValue(entity.Name, out var personCache))
- {
- peopleCache[entity.Name] = personCache = (entity, []);
- }
- if (reader.TryGetString(2, out var role))
- {
- }
- if (reader.TryGetInt32(4, out var sortOrder))
- {
- }
- personCache.Items.Add(new PeopleBaseItemMap()
- {
- Item = null!,
- ItemId = itemId,
- People = null!,
- PeopleId = personCache.Person.Id,
- ListOrder = sortOrder,
- SortOrder = sortOrder,
- Role = role
- });
- }
- baseItemIds.Clear();
- foreach (var item in peopleCache)
- {
- dbContext.Peoples.Add(item.Value.Person);
- dbContext.PeopleBaseItemMap.AddRange(item.Value.Items.DistinctBy(e => (e.ItemId, e.PeopleId)));
- }
- peopleCache.Clear();
- _logger.LogInformation("Try saving {0} People entries.", dbContext.MediaStreamInfos.Local.Count);
- dbContext.SaveChanges();
- migrationTotalTime += stopwatch.Elapsed;
- _logger.LogInformation("Saving People entries took {0}.", stopwatch.Elapsed);
- stopwatch.Restart();
- _logger.LogInformation("Start 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)
- """;
- dbContext.Chapters.ExecuteDelete();
- foreach (SqliteDataReader dto in connection.Query(chapterQuery))
- {
- var chapter = GetChapter(dto);
- dbContext.Chapters.Add(chapter);
- }
- _logger.LogInformation("Try saving {0} Chapters entries.", dbContext.Chapters.Local.Count);
- dbContext.SaveChanges();
- migrationTotalTime += stopwatch.Elapsed;
- _logger.LogInformation("Saving Chapters took {0}.", stopwatch.Elapsed);
- stopwatch.Restart();
- _logger.LogInformation("Start 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)
- """;
- dbContext.Chapters.ExecuteDelete();
- foreach (SqliteDataReader dto in connection.Query(ancestorIdsQuery))
- {
- var ancestorId = GetAncestorId(dto);
- dbContext.AncestorIds.Add(ancestorId);
- }
- _logger.LogInformation("Try saving {0} AncestorIds entries.", dbContext.Chapters.Local.Count);
- dbContext.SaveChanges();
- migrationTotalTime += stopwatch.Elapsed;
- _logger.LogInformation("Saving AncestorIds took {0}.", stopwatch.Elapsed);
- stopwatch.Restart();
- connection.Close();
- _logger.LogInformation("Migration of the Library.db done.");
- _logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old");
- SqliteConnection.ClearAllPools();
- File.Move(libraryDbPath, libraryDbPath + ".old");
- _logger.LogInformation("Migrating Library db took {0}.", migrationTotalTime);
- _jellyfinDatabaseProvider.RunScheduledOptimisation(CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult();
- }
- private UserData? GetUserData(ImmutableArray<User> users, SqliteDataReader dto)
- {
- var internalUserId = dto.GetInt32(1);
- var user = users.FirstOrDefault(e => e.InternalId == internalUserId);
- if (user is null)
- {
- _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;
- }
- 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("="))
- .Select(e => new BaseItemProvider()
- {
- Item = null!,
- ProviderId = e[0],
- ProviderValue = e[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;
- }
- var baseItem = BaseItemRepository.DeserialiseBaseItem(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 != 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;
- }
- }
|