2025-01-15 20:12:41 +00:00
#pragma warning disable RS0030 // Do not use banned APIs
2024-10-09 17:04:58 +00:00
using System ;
2024-10-20 10:11:24 +00:00
using System.Collections.Generic ;
2024-10-09 17:04:58 +00:00
using System.Collections.Immutable ;
2024-10-11 11:11:15 +00:00
using System.Data ;
using System.Diagnostics ;
2024-10-09 23:01:54 +00:00
using System.Globalization ;
2024-10-09 17:04:58 +00:00
using System.IO ;
using System.Linq ;
2024-10-09 23:01:54 +00:00
using System.Text ;
2025-01-26 20:45:28 +00:00
using System.Threading ;
2024-10-09 17:04:58 +00:00
using Emby.Server.Implementations.Data ;
2025-03-25 15:30:22 +00:00
using Jellyfin.Database.Implementations ;
2025-03-25 16:45:00 +01:00
using Jellyfin.Database.Implementations.Entities ;
2024-10-09 23:01:54 +00:00
using Jellyfin.Extensions ;
2024-11-12 15:37:01 +00:00
using Jellyfin.Server.Implementations.Item ;
2024-10-09 17:04:58 +00:00
using MediaBrowser.Controller ;
2025-04-28 03:18:08 +03:00
using MediaBrowser.Controller.Channels ;
using MediaBrowser.Controller.Chapters ;
2024-10-09 23:01:54 +00:00
using MediaBrowser.Controller.Entities ;
2025-04-28 03:18:08 +03:00
using MediaBrowser.Controller.Library ;
using MediaBrowser.Controller.LiveTv ;
using MediaBrowser.Controller.Persistence ;
using MediaBrowser.Controller.Providers ;
2024-10-09 17:04:58 +00:00
using MediaBrowser.Model.Entities ;
2025-04-28 03:18:08 +03:00
using MediaBrowser.Model.Globalization ;
using MediaBrowser.Model.IO ;
2024-10-09 17:04:58 +00:00
using Microsoft.Data.Sqlite ;
using Microsoft.EntityFrameworkCore ;
2025-04-28 03:18:08 +03:00
using Microsoft.Extensions.DependencyInjection ;
2024-10-09 17:04:58 +00:00
using Microsoft.Extensions.Logging ;
2025-03-25 16:45:00 +01:00
using BaseItemEntity = Jellyfin . Database . Implementations . Entities . BaseItemEntity ;
using Chapter = Jellyfin . Database . Implementations . Entities . Chapter ;
2024-10-09 17:04:58 +00:00
namespace Jellyfin.Server.Migrations.Routines ;
/// <summary>
/// The migration routine for migrating the userdata database to EF Core.
/// </summary>
2025-04-28 03:18:08 +03:00
[JellyfinMigration("2025-04-20T20:00:00", nameof(MigrateLibraryDb), "36445464-849f-429f-9ad0-bb130efa0664")]
2025-03-27 03:23:36 +01:00
internal class MigrateLibraryDb : IDatabaseMigrationRoutine
2024-10-09 17:04:58 +00:00
{
private const string DbFilename = "library.db" ;
2024-10-10 18:30:08 +00:00
private readonly ILogger < MigrateLibraryDb > _logger ;
2024-10-09 17:04:58 +00:00
private readonly IServerApplicationPaths _paths ;
2025-01-26 20:45:28 +00:00
private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider ;
2024-10-09 17:04:58 +00:00
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>
2025-01-26 20:45:28 +00:00
/// <param name="jellyfinDatabaseProvider">The database provider for special access.</param>
2025-04-28 03:18:08 +03:00
/// <param name="serviceProvider">The Service provider.</param>
2024-10-09 17:04:58 +00:00
public MigrateLibraryDb (
2024-10-10 18:30:08 +00:00
ILogger < MigrateLibraryDb > logger ,
2024-10-09 17:04:58 +00:00
IDbContextFactory < JellyfinDbContext > provider ,
2025-01-26 20:45:28 +00:00
IServerApplicationPaths paths ,
2025-04-28 03:18:08 +03:00
IJellyfinDatabaseProvider jellyfinDatabaseProvider ,
IServiceProvider serviceProvider )
2024-10-09 17:04:58 +00:00
{
_logger = logger ;
_provider = provider ;
_paths = paths ;
2025-01-26 20:45:28 +00:00
_jellyfinDatabaseProvider = jellyfinDatabaseProvider ;
2024-10-09 17:04:58 +00:00
}
/// <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 ) ;
2025-04-28 03:18:08 +03:00
if ( ! File . Exists ( libraryDbPath ) )
{
_logger . LogError ( "Cannot migrate {LibraryDb} as it does not exist.." , libraryDbPath ) ;
return ;
}
2025-03-31 05:36:27 +02:00
using var connection = new SqliteConnection ( $"Filename={libraryDbPath};Mode=ReadOnly" ) ;
2024-11-10 19:25:17 +00:00
2025-03-31 05:36:27 +02:00
var fullOperationTimer = new Stopwatch ( ) ;
fullOperationTimer . Start ( ) ;
2024-10-09 17:04:58 +00:00
2025-03-31 05:36:27 +02:00
using ( var operation = GetPreparedDbContext ( "Cleanup database" ) )
{
2025-04-03 17:17:14 +02:00
operation . JellyfinDbContext . AttachmentStreamInfos . ExecuteDelete ( ) ;
2025-03-31 05:36:27 +02:00
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 ( ) ;
}
2024-10-20 10:11:24 +00:00
var legacyBaseItemWithUserKeys = new Dictionary < string , BaseItemEntity > ( ) ;
2025-03-31 05:36:27 +02:00
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 ) )
2024-11-12 15:37:01 +00:00
{
2025-03-31 05:36:27 +02:00
operation . JellyfinDbContext . SaveChanges ( ) ;
2024-11-12 15:37:01 +00:00
}
2024-10-20 10:11:24 +00:00
}
2025-03-31 05:36:27 +02:00
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 )
"" ";
2024-10-20 10:11:24 +00:00
2025-03-31 05:36:27 +02:00
// EFCores local lookup sucks. We cannot use context.ItemValues.Local here because its just super slow.
2025-04-07 22:42:01 +02:00
var localItems = new Dictionary < ( int Type , string Value ) , ( Database . Implementations . Entities . ItemValue ItemValue , List < Guid > ItemIds ) > ( ) ;
2025-03-31 05:36:27 +02:00
using ( new TrackedMigrationStep ( "loading ItemValues" , _logger ) )
{
foreach ( SqliteDataReader dto in connection . Query ( itemValueQuery ) )
{
var itemId = dto . GetGuid ( 0 ) ;
var entity = GetItemValue ( dto ) ;
2025-04-07 22:42:01 +02:00
var key = ( ( int ) entity . Type , entity . Value ) ;
2025-03-31 05:36:27 +02:00
if ( ! localItems . TryGetValue ( key , out var existing ) )
{
localItems [ key ] = existing = ( entity , [ ] ) ;
}
2024-11-13 14:25:26 +00:00
2025-03-31 05:36:27 +02:00
existing . ItemIds . Add ( itemId ) ;
}
2024-11-13 14:25:26 +00:00
2025-03-31 05:36:27 +02:00
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
} ) ) ;
}
2024-11-13 14:25:26 +00:00
}
2025-03-31 05:36:27 +02:00
using ( new TrackedMigrationStep ( $"saving {operation.JellyfinDbContext.ItemValues.Local.Count} ItemValues entries" , _logger ) )
{
operation . JellyfinDbContext . SaveChanges ( ) ;
}
2024-11-13 14:25:26 +00:00
}
2025-03-31 05:36:27 +02:00
using ( var operation = GetPreparedDbContext ( "moving UserData" ) )
2024-11-13 14:25:26 +00:00
{
2025-03-31 05:36:27 +02:00
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 ) )
2024-11-13 14:25:26 +00:00
{
2025-03-31 05:36:27 +02:00
var users = operation . JellyfinDbContext . Users . AsNoTracking ( ) . ToImmutableArray ( ) ;
var userIdBlacklist = new HashSet < int > ( ) ;
2024-11-13 14:25:26 +00:00
2025-03-31 05:36:27 +02:00
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 ) ;
2024-11-13 14:25:26 +00:00
2025-03-31 05:36:27 +02:00
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 ) ;
}
2024-11-23 22:39:39 +00:00
2025-03-31 05:36:27 +02:00
continue ;
}
2024-10-09 17:04:58 +00:00
2025-03-31 05:36:27 +02:00
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 ;
}
2024-10-09 17:04:58 +00:00
2025-03-31 05:36:27 +02:00
userData . ItemId = refItem . Id ;
operation . JellyfinDbContext . UserData . Add ( userData ) ;
}
2024-10-09 17:04:58 +00:00
2025-03-31 05:36:27 +02:00
users . Clear ( ) ;
2024-10-11 11:11:15 +00:00
}
2025-03-31 05:36:27 +02:00
legacyBaseItemWithUserKeys . Clear ( ) ;
using ( new TrackedMigrationStep ( $"saving {operation.JellyfinDbContext.UserData.Local.Count} UserData entries" , _logger ) )
2024-10-20 10:11:24 +00:00
{
2025-03-31 05:36:27 +02:00
operation . JellyfinDbContext . SaveChanges ( ) ;
2024-10-20 10:11:24 +00:00
}
2024-10-09 17:04:58 +00:00
}
2025-03-31 05:36:27 +02:00
using ( var operation = GetPreparedDbContext ( "moving MediaStreamInfos" ) )
2024-10-09 17:04:58 +00:00
{
2025-03-31 05:36:27 +02:00
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 )
"" ";
2024-10-09 17:04:58 +00:00
2025-03-31 05:36:27 +02:00
using ( new TrackedMigrationStep ( "loading MediaStreamInfos" , _logger ) )
2024-10-11 11:11:15 +00:00
{
2025-03-31 05:36:27 +02:00
foreach ( SqliteDataReader dto in connection . Query ( mediaStreamQuery ) )
{
operation . JellyfinDbContext . MediaStreamInfos . Add ( GetMediaStream ( dto ) ) ;
}
2024-10-11 11:11:15 +00:00
}
2025-03-31 05:36:27 +02:00
using ( new TrackedMigrationStep ( $"saving {operation.JellyfinDbContext.MediaStreamInfos.Local.Count} MediaStreamInfos entries" , _logger ) )
2024-10-11 11:11:15 +00:00
{
2025-03-31 05:36:27 +02:00
operation . JellyfinDbContext . SaveChanges ( ) ;
2024-10-11 11:11:15 +00:00
}
2025-03-31 05:36:27 +02:00
}
2024-10-11 11:11:15 +00:00
2025-04-03 17:17:14 +02:00
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 ( ) ;
}
}
2025-03-31 05:36:27 +02:00
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 )
"" ";
2024-10-11 11:11:15 +00:00
2025-03-31 05:36:27 +02:00
var peopleCache = new Dictionary < string , ( People Person , List < PeopleBaseItemMap > Items ) > ( ) ;
2024-10-11 11:11:15 +00:00
2025-03-31 05:36:27 +02:00
using ( new TrackedMigrationStep ( "loading People" , _logger ) )
2024-10-11 11:11:15 +00:00
{
2025-03-31 05:36:27 +02:00
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 ;
}
2024-10-09 17:04:58 +00:00
2025-03-31 05:36:27 +02:00
var entity = GetPerson ( reader ) ;
if ( ! peopleCache . TryGetValue ( entity . Name , out var personCache ) )
{
peopleCache [ entity . Name ] = personCache = ( entity , [ ] ) ;
}
2025-03-23 01:30:32 +01:00
2025-03-31 05:36:27 +02:00
if ( reader . TryGetString ( 2 , out var role ) )
{
}
2024-10-09 17:04:58 +00:00
2025-03-31 05:36:27 +02:00
int? sortOrder = reader . IsDBNull ( 4 ) ? null : reader . GetInt32 ( 4 ) ;
2025-03-23 01:30:32 +01:00
2025-03-31 05:36:27 +02:00
personCache . Items . Add ( new PeopleBaseItemMap ( )
{
Item = null ! ,
ItemId = itemId ,
People = null ! ,
PeopleId = personCache . Person . Id ,
ListOrder = sortOrder ,
SortOrder = sortOrder ,
Role = role
} ) ;
}
2024-10-09 17:04:58 +00:00
2025-03-31 05:36:27 +02:00
baseItemIds . Clear ( ) ;
2024-10-09 17:04:58 +00:00
2025-03-31 05:36:27 +02:00
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 ) ) ) ;
}
2024-10-09 17:04:58 +00:00
2025-03-31 05:36:27 +02:00
peopleCache . Clear ( ) ;
}
2024-10-09 17:04:58 +00:00
2025-03-31 05:36:27 +02:00
using ( new TrackedMigrationStep ( $"saving {operation.JellyfinDbContext.Peoples.Local.Count} People entries and {operation.JellyfinDbContext.PeopleBaseItemMap.Local.Count} maps" , _logger ) )
{
operation . JellyfinDbContext . SaveChanges ( ) ;
}
}
2024-10-09 17:04:58 +00:00
2025-03-31 05:36:27 +02:00
using ( var operation = GetPreparedDbContext ( "moving Chapters" ) )
2024-10-09 17:04:58 +00:00
{
2025-03-31 05:36:27 +02:00
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 ( ) ;
}
2024-10-09 17:04:58 +00:00
}
2025-03-31 05:36:27 +02:00
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 )
"" ";
2024-10-11 11:11:15 +00:00
2025-03-31 05:36:27 +02:00
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 ( ) ;
}
}
2024-10-09 17:04:58 +00:00
connection . Close ( ) ;
2025-03-31 05:36:27 +02:00
2024-10-20 09:43:40 +00:00
_logger . LogInformation ( "Migration of the Library.db done." ) ;
2025-03-31 05:36:27 +02:00
_logger . LogInformation ( "Migrating Library db took {0}." , fullOperationTimer . Elapsed ) ;
2024-11-12 07:16:24 +00:00
SqliteConnection . ClearAllPools ( ) ;
2025-03-27 12:34:59 +01:00
2025-03-31 05:36:27 +02:00
_logger . LogInformation ( "Move {0} to {1}." , libraryDbPath , libraryDbPath + ".old" ) ;
2025-03-27 12:34:59 +01:00
File . Move ( libraryDbPath , libraryDbPath + ".old" , true ) ;
2024-10-09 17:04:58 +00:00
}
2025-03-31 05:36:27 +02:00
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 )
2024-10-09 17:04:58 +00:00
{
2024-11-23 22:39:39 +00:00
var internalUserId = dto . GetInt32 ( 1 ) ;
var user = users . FirstOrDefault ( e = > e . InternalId = = internalUserId ) ;
2024-11-10 18:36:46 +00:00
if ( user is null )
2024-10-11 11:11:15 +00:00
{
2025-03-31 05:36:27 +02:00
if ( userIdBlacklist . Contains ( internalUserId ) )
{
return null ;
}
2024-11-23 22:39:39 +00:00
_logger . LogError ( "Tried to find user with index '{Idx}' but there are only '{MaxIdx}' users." , internalUserId , users . Length ) ;
2024-11-11 17:39:50 +00:00
return null ;
2024-10-11 11:11:15 +00:00
}
2024-10-20 10:11:24 +00:00
var oldKey = dto . GetString ( 0 ) ;
2024-11-11 17:39:50 +00:00
return new UserData ( )
2024-10-09 17:04:58 +00:00
{
2024-10-20 10:11:24 +00:00
ItemId = Guid . NewGuid ( ) ,
2024-11-11 17:39:50 +00:00
CustomDataKey = oldKey ,
2024-11-10 18:36:46 +00:00
UserId = user . Id ,
2024-10-09 17:04:58 +00:00
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 ! ,
2024-10-20 10:11:24 +00:00
Item = null !
2024-11-11 17:39:50 +00:00
} ;
2024-10-09 17:04:58 +00:00
}
private AncestorId GetAncestorId ( SqliteDataReader reader )
{
return new AncestorId ( )
{
ItemId = reader . GetGuid ( 0 ) ,
2024-10-10 14:32:49 +00:00
ParentItemId = reader . GetGuid ( 1 ) ,
Item = null ! ,
ParentItem = null !
2024-10-09 17:04:58 +00:00
} ;
}
/// <summary>
/// Gets the chapter.
/// </summary>
/// <param name="reader">The reader.</param>
/// <returns>ChapterInfo.</returns>
private Chapter GetChapter ( SqliteDataReader reader )
{
var chapter = new Chapter
{
2024-10-11 11:11:15 +00:00
StartPositionTicks = reader . GetInt64 ( 1 ) ,
ChapterIndex = reader . GetInt32 ( 5 ) ,
2024-10-09 17:04:58 +00:00
Item = null ! ,
2024-10-11 11:11:15 +00:00
ItemId = reader . GetGuid ( 0 ) ,
2024-10-09 17:04:58 +00:00
} ;
2024-10-11 11:11:15 +00:00
if ( reader . TryGetString ( 2 , out var chapterName ) )
2024-10-09 17:04:58 +00:00
{
chapter . Name = chapterName ;
}
2024-10-11 11:11:15 +00:00
if ( reader . TryGetString ( 3 , out var imagePath ) )
2024-10-09 17:04:58 +00:00
{
chapter . ImagePath = imagePath ;
}
2024-10-11 11:11:15 +00:00
if ( reader . TryReadDateTime ( 4 , out var imageDateModified ) )
2024-10-09 17:04:58 +00:00
{
chapter . ImageDateModified = imageDateModified ;
}
return chapter ;
}
private ItemValue GetItemValue ( SqliteDataReader reader )
{
return new ItemValue
{
2024-10-10 14:32:49 +00:00
ItemValueId = Guid . NewGuid ( ) ,
2024-10-09 23:55:28 +00:00
Type = ( ItemValueType ) reader . GetInt32 ( 1 ) ,
2024-10-09 17:04:58 +00:00
Value = reader . GetString ( 2 ) ,
CleanValue = reader . GetString ( 3 ) ,
} ;
}
private People GetPerson ( SqliteDataReader reader )
{
var item = new People
{
2024-10-11 11:11:15 +00:00
Id = Guid . NewGuid ( ) ,
2024-10-09 17:04:58 +00:00
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 ) ,
2024-10-09 23:19:24 +00:00
StreamType = Enum . Parse < MediaStreamTypeEntity > ( reader . GetString ( 2 ) ) ,
2024-10-09 17:04:58 +00:00
Item = null ! ,
ItemId = reader . GetGuid ( 0 ) ,
2024-11-12 23:53:05 +00:00
AspectRatio = null ! ,
ChannelLayout = null ! ,
Codec = null ! ,
IsInterlaced = false ,
Language = null ! ,
Path = null ! ,
Profile = null ! ,
2024-10-09 17:04:58 +00:00
} ;
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 ;
2024-10-11 11:11:15 +00:00
// if (reader.TryGetInt32(44, out var rotation))
// {
// item.Rotation = rotation;
// }
2024-10-09 17:04:58 +00:00
return item ;
}
2025-04-03 17:17:14 +02:00
/// <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 ;
}
2024-11-12 15:37:01 +00:00
private ( BaseItemEntity BaseItem , string [ ] LegacyUserDataKey ) GetItem ( SqliteDataReader reader )
2024-10-09 17:04:58 +00:00
{
2024-10-09 23:01:54 +00:00
var entity = new BaseItemEntity ( )
2024-10-09 17:04:58 +00:00
{
2024-11-10 18:01:51 +00:00
Id = reader . GetGuid ( 0 ) ,
Type = reader . GetString ( 1 ) ,
2024-10-09 17:04:58 +00:00
} ;
2024-10-28 09:02:24 +00:00
var index = 2 ;
2024-10-09 17:04:58 +00:00
if ( reader . TryGetString ( index + + , out var data ) )
{
2024-10-09 23:01:54 +00:00
entity . Data = data ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryReadDateTime ( index + + , out var startDate ) )
{
2024-10-09 23:01:54 +00:00
entity . StartDate = startDate ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryReadDateTime ( index + + , out var endDate ) )
{
2024-10-09 23:01:54 +00:00
entity . EndDate = endDate ;
2024-10-09 17:04:58 +00:00
}
2025-02-13 20:17:25 -07:00
if ( reader . TryGetGuid ( index + + , out var guid ) )
2024-10-09 17:04:58 +00:00
{
2024-10-28 09:24:12 +00:00
entity . ChannelId = guid ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetBoolean ( index + + , out var isMovie ) )
{
2024-10-09 23:01:54 +00:00
entity . IsMovie = isMovie ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetBoolean ( index + + , out var isSeries ) )
{
2024-10-09 23:01:54 +00:00
entity . IsSeries = isSeries ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var episodeTitle ) )
{
2024-10-09 23:01:54 +00:00
entity . EpisodeTitle = episodeTitle ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetBoolean ( index + + , out var isRepeat ) )
{
2024-10-09 23:01:54 +00:00
entity . IsRepeat = isRepeat ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetSingle ( index + + , out var communityRating ) )
{
2024-10-09 23:01:54 +00:00
entity . CommunityRating = communityRating ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var customRating ) )
{
2024-10-09 23:01:54 +00:00
entity . CustomRating = customRating ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetInt32 ( index + + , out var indexNumber ) )
{
2024-10-09 23:01:54 +00:00
entity . IndexNumber = indexNumber ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetBoolean ( index + + , out var isLocked ) )
{
2024-10-09 23:01:54 +00:00
entity . IsLocked = isLocked ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var preferredMetadataLanguage ) )
{
2024-10-09 23:01:54 +00:00
entity . PreferredMetadataLanguage = preferredMetadataLanguage ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var preferredMetadataCountryCode ) )
{
2024-10-09 23:01:54 +00:00
entity . PreferredMetadataCountryCode = preferredMetadataCountryCode ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetInt32 ( index + + , out var width ) )
{
2024-10-09 23:01:54 +00:00
entity . Width = width ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetInt32 ( index + + , out var height ) )
{
2024-10-09 23:01:54 +00:00
entity . Height = height ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryReadDateTime ( index + + , out var dateLastRefreshed ) )
{
2024-10-09 23:01:54 +00:00
entity . DateLastRefreshed = dateLastRefreshed ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var name ) )
{
2024-10-09 23:01:54 +00:00
entity . Name = name ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var restorePath ) )
{
2024-10-09 23:01:54 +00:00
entity . Path = restorePath ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryReadDateTime ( index + + , out var premiereDate ) )
{
2024-10-09 23:01:54 +00:00
entity . PremiereDate = premiereDate ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var overview ) )
{
2024-10-09 23:01:54 +00:00
entity . Overview = overview ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetInt32 ( index + + , out var parentIndexNumber ) )
{
2024-10-09 23:01:54 +00:00
entity . ParentIndexNumber = parentIndexNumber ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetInt32 ( index + + , out var productionYear ) )
{
2024-10-09 23:01:54 +00:00
entity . ProductionYear = productionYear ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var officialRating ) )
{
2024-10-09 23:01:54 +00:00
entity . OfficialRating = officialRating ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var forcedSortName ) )
{
2024-10-09 23:01:54 +00:00
entity . ForcedSortName = forcedSortName ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetInt64 ( index + + , out var runTimeTicks ) )
{
2024-10-09 23:01:54 +00:00
entity . RunTimeTicks = runTimeTicks ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetInt64 ( index + + , out var size ) )
{
2024-10-09 23:01:54 +00:00
entity . Size = size ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryReadDateTime ( index + + , out var dateCreated ) )
{
2024-10-09 23:01:54 +00:00
entity . DateCreated = dateCreated ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryReadDateTime ( index + + , out var dateModified ) )
{
2024-10-09 23:01:54 +00:00
entity . DateModified = dateModified ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var genres ) )
{
2024-10-09 23:01:54 +00:00
entity . Genres = genres ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetGuid ( index + + , out var parentId ) )
{
2024-10-09 23:01:54 +00:00
entity . ParentId = parentId ;
2024-10-09 17:04:58 +00:00
}
2024-11-11 00:27:30 +00:00
if ( reader . TryGetGuid ( index + + , out var topParentId ) )
{
entity . TopParentId = topParentId ;
}
2024-10-09 23:01:54 +00:00
if ( reader . TryGetString ( index + + , out var audioString ) & & Enum . TryParse < ProgramAudioEntity > ( audioString , out var audioType ) )
2024-10-09 17:04:58 +00:00
{
2024-10-09 23:01:54 +00:00
entity . Audio = audioType ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var serviceName ) )
{
2024-10-09 23:01:54 +00:00
entity . ExternalServiceId = serviceName ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetBoolean ( index + + , out var isInMixedFolder ) )
{
2024-10-09 23:01:54 +00:00
entity . IsInMixedFolder = isInMixedFolder ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryReadDateTime ( index + + , out var dateLastSaved ) )
{
2024-10-09 23:01:54 +00:00
entity . DateLastSaved = dateLastSaved ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var lockedFields ) )
{
2024-10-09 23:01:54 +00:00
entity . LockedFields = lockedFields . Split ( '|' ) . Select ( Enum . Parse < MetadataField > )
. Select ( e = > new BaseItemMetadataField ( )
{
Id = ( int ) e ,
Item = entity ,
ItemId = entity . Id
} )
. ToArray ( ) ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var studios ) )
{
2024-10-09 23:01:54 +00:00
entity . Studios = studios ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var tags ) )
{
2024-10-09 23:01:54 +00:00
entity . Tags = tags ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var trailerTypes ) )
{
2024-10-09 23:01:54 +00:00
entity . TrailerTypes = trailerTypes . Split ( '|' ) . Select ( Enum . Parse < TrailerType > )
. Select ( e = > new BaseItemTrailerType ( )
{
Id = ( int ) e ,
Item = entity ,
ItemId = entity . Id
} )
. ToArray ( ) ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var originalTitle ) )
{
2024-10-09 23:01:54 +00:00
entity . OriginalTitle = originalTitle ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var primaryVersionId ) )
{
2024-10-09 23:01:54 +00:00
entity . PrimaryVersionId = primaryVersionId ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryReadDateTime ( index + + , out var dateLastMediaAdded ) )
{
2024-10-09 23:01:54 +00:00
entity . DateLastMediaAdded = dateLastMediaAdded ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var album ) )
{
2024-10-09 23:01:54 +00:00
entity . Album = album ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetSingle ( index + + , out var lUFS ) )
{
2024-10-09 23:01:54 +00:00
entity . LUFS = lUFS ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetSingle ( index + + , out var normalizationGain ) )
{
2024-10-09 23:01:54 +00:00
entity . NormalizationGain = normalizationGain ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetSingle ( index + + , out var criticRating ) )
{
2024-10-09 23:01:54 +00:00
entity . CriticRating = criticRating ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetBoolean ( index + + , out var isVirtualItem ) )
{
2024-10-09 23:01:54 +00:00
entity . IsVirtualItem = isVirtualItem ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var seriesName ) )
{
2024-10-09 23:01:54 +00:00
entity . SeriesName = seriesName ;
2024-10-09 17:04:58 +00:00
}
2024-11-12 15:37:01 +00:00
var userDataKeys = new List < string > ( ) ;
if ( reader . TryGetString ( index + + , out var directUserDataKey ) )
2024-10-20 10:11:24 +00:00
{
2024-11-12 15:37:01 +00:00
userDataKeys . Add ( directUserDataKey ) ;
2024-10-20 10:11:24 +00:00
}
2024-10-09 17:04:58 +00:00
if ( reader . TryGetString ( index + + , out var seasonName ) )
{
2024-10-09 23:01:54 +00:00
entity . SeasonName = seasonName ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetGuid ( index + + , out var seasonId ) )
{
2024-10-09 23:01:54 +00:00
entity . SeasonId = seasonId ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetGuid ( index + + , out var seriesId ) )
{
2024-10-09 23:01:54 +00:00
entity . SeriesId = seriesId ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var presentationUniqueKey ) )
{
2024-10-09 23:01:54 +00:00
entity . PresentationUniqueKey = presentationUniqueKey ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetInt32 ( index + + , out var parentalRating ) )
{
2024-10-09 23:01:54 +00:00
entity . InheritedParentalRatingValue = parentalRating ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var externalSeriesId ) )
{
2024-10-09 23:01:54 +00:00
entity . ExternalSeriesId = externalSeriesId ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var tagLine ) )
{
2024-10-09 23:01:54 +00:00
entity . Tagline = tagLine ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var providerIds ) )
{
2024-10-09 23:01:54 +00:00
entity . Provider = providerIds . Split ( '|' ) . Select ( e = > e . Split ( "=" ) )
2024-10-09 17:04:58 +00:00
. Select ( e = > new BaseItemProvider ( )
{
Item = null ! ,
ProviderId = e [ 0 ] ,
ProviderValue = e [ 1 ]
} ) . ToArray ( ) ;
}
if ( reader . TryGetString ( index + + , out var imageInfos ) )
{
2024-10-09 23:01:54 +00:00
entity . Images = DeserializeImages ( imageInfos ) . Select ( f = > Map ( entity . Id , f ) ) . ToArray ( ) ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var productionLocations ) )
{
2024-10-09 23:01:54 +00:00
entity . ProductionLocations = productionLocations ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var extraIds ) )
{
2024-10-09 23:01:54 +00:00
entity . ExtraIds = extraIds ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetInt32 ( index + + , out var totalBitrate ) )
{
2024-10-09 23:01:54 +00:00
entity . TotalBitrate = totalBitrate ;
2024-10-09 17:04:58 +00:00
}
2024-10-09 23:01:54 +00:00
if ( reader . TryGetString ( index + + , out var extraTypeString ) & & Enum . TryParse < BaseItemExtraType > ( extraTypeString , out var extraType ) )
2024-10-09 17:04:58 +00:00
{
2024-10-09 23:01:54 +00:00
entity . ExtraType = extraType ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var artists ) )
{
2024-10-09 23:01:54 +00:00
entity . Artists = artists ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var albumArtists ) )
{
2024-10-09 23:01:54 +00:00
entity . AlbumArtists = albumArtists ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var externalId ) )
{
2024-10-09 23:01:54 +00:00
entity . ExternalId = externalId ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var seriesPresentationUniqueKey ) )
{
2024-10-09 23:01:54 +00:00
entity . SeriesPresentationUniqueKey = seriesPresentationUniqueKey ;
2024-10-09 17:04:58 +00:00
}
if ( reader . TryGetString ( index + + , out var showId ) )
{
2024-10-09 23:01:54 +00:00
entity . ShowId = showId ;
2024-10-09 17:04:58 +00:00
}
2024-10-28 09:24:12 +00:00
if ( reader . TryGetString ( index + + , out var ownerId ) )
2024-10-09 17:04:58 +00:00
{
2024-10-28 09:24:12 +00:00
entity . OwnerId = ownerId ;
2024-10-09 17:04:58 +00:00
}
2024-11-12 15:37:01 +00:00
if ( reader . TryGetString ( index + + , out var mediaType ) )
{
entity . MediaType = mediaType ;
}
2025-03-08 13:55:21 +03:00
if ( reader . TryGetString ( index + + , out var sortName ) )
{
entity . SortName = sortName ;
}
2025-03-10 11:50:28 -04:00
if ( reader . TryGetString ( index + + , out var cleanName ) )
{
entity . CleanName = cleanName ;
}
2025-03-27 16:43:39 +00:00
if ( reader . TryGetString ( index + + , out var unratedType ) )
{
entity . UnratedType = unratedType ;
}
2024-11-12 17:23:41 +00:00
var baseItem = BaseItemRepository . DeserialiseBaseItem ( entity , _logger , null , false ) ;
2024-11-12 15:37:01 +00:00
var dataKeys = baseItem . GetUserDataKeys ( ) ;
userDataKeys . AddRange ( dataKeys ) ;
return ( entity , userDataKeys . ToArray ( ) ) ;
2024-10-09 23:01:54 +00:00
}
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 ;
2024-10-09 17:04:58 +00:00
}
2025-03-31 05:36:27 +02:00
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 ( ) ;
}
}
2024-10-09 17:04:58 +00:00
}