1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399 |
- #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.Database.Implementations;
- using Jellyfin.Database.Implementations.Entities;
- using Jellyfin.Extensions;
- using Jellyfin.Server.Implementations.Item;
- using MediaBrowser.Controller;
- using MediaBrowser.Controller.Channels;
- using MediaBrowser.Controller.Chapters;
- using MediaBrowser.Controller.Entities;
- using MediaBrowser.Controller.Library;
- using MediaBrowser.Controller.LiveTv;
- using MediaBrowser.Controller.Persistence;
- using MediaBrowser.Controller.Providers;
- using MediaBrowser.Model.Entities;
- using MediaBrowser.Model.Globalization;
- using MediaBrowser.Model.IO;
- using Microsoft.Data.Sqlite;
- using Microsoft.EntityFrameworkCore;
- using Microsoft.Extensions.DependencyInjection;
- 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), "36445464-849f-429f-9ad0-bb130efa0664")]
- internal class MigrateLibraryDb : IDatabaseMigrationRoutine
- {
- 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>
- /// <param name="serviceProvider">The Service provider.</param>
- public MigrateLibraryDb(
- ILogger<MigrateLibraryDb> logger,
- IDbContextFactory<JellyfinDbContext> provider,
- IServerApplicationPaths paths,
- IJellyfinDatabaseProvider jellyfinDatabaseProvider,
- IServiceProvider serviceProvider)
- {
- _logger = logger;
- _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();
- }
- var legacyBaseItemWithUserKeys = new Dictionary<string, BaseItemEntity>();
- connection.Open();
- var baseItemIds = new HashSet<Guid>();
- using (var operation = GetPreparedDbContext("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, UnratedType FROM TypedBaseItems
- """;
- using (new TrackedMigrationStep("Loading TypedBaseItems", _logger))
- {
- foreach (SqliteDataReader dto in connection.Query(typedBaseItemsQuery))
- {
- var baseItem = GetItem(dto);
- operation.JellyfinDbContext.BaseItems.Add(baseItem.BaseItem);
- baseItemIds.Add(baseItem.BaseItem.Id);
- foreach (var dataKey in baseItem.LegacyUserDataKey)
- {
- legacyBaseItemWithUserKeys[dataKey] = baseItem.BaseItem;
- }
- }
- }
- using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.BaseItems.Local.Count} BaseItem entries", _logger))
- {
- operation.JellyfinDbContext.SaveChanges();
- }
- }
- 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);
- 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().ToImmutableArray();
- var userIdBlacklist = new HashSet<int>();
- foreach (var entity in queryResult)
- {
- var userData = GetUserData(users, entity, userIdBlacklist);
- 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;
- }
- userData.ItemId = refItem.Id;
- operation.JellyfinDbContext.UserData.Add(userData);
- }
- users.Clear();
- }
- 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))
- {
- operation.JellyfinDbContext.MediaStreamInfos.Add(GetMediaStream(dto));
- }
- }
- 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))
- {
- operation.JellyfinDbContext.AttachmentStreamInfos.Add(GetMediaAttachment(dto));
- }
- }
- 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 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("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))
- {
- }
- int? sortOrder = reader.IsDBNull(4) ? null : reader.GetInt32(4);
- 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)
- {
- 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);
- 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);
- 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);
- }
- private UserData? GetUserData(ImmutableArray<User> users, SqliteDataReader dto, HashSet<int> userIdBlacklist)
- {
- var internalUserId = dto.GetInt32(1);
- var user = users.FirstOrDefault(e => e.InternalId == internalUserId);
- if (user is null)
- {
- if (userIdBlacklist.Contains(internalUserId))
- {
- return 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;
- }
- /// <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("="))
- .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;
- }
- if (reader.TryGetString(index++, out var unratedType))
- {
- entity.UnratedType = unratedType;
- }
- 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;
- }
- 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();
- }
- }
- }
|