aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Emby.Server.Implementations/Data/ItemTypeLookup.cs41
-rw-r--r--Jellyfin.Data/Entities/BaseItemEntity.cs22
-rw-r--r--Jellyfin.Data/Entities/BaseItemExtraType.cs18
-rw-r--r--Jellyfin.Data/Entities/BaseItemImageInfo.cs57
-rw-r--r--Jellyfin.Data/Entities/BaseItemMetadataField.cs26
-rw-r--r--Jellyfin.Data/Entities/BaseItemTrailerType.cs25
-rw-r--r--Jellyfin.Data/Entities/EnumLikeTable.cs14
-rw-r--r--Jellyfin.Data/Entities/ImageInfoImageType.cs76
-rw-r--r--Jellyfin.Data/Entities/ProgramAudioEntity.cs37
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.cs316
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDbContext.cs15
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20241009225800_ExpandedBaseItemFields.Designer.cs1540
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20241009225800_ExpandedBaseItemFields.cs169
-rw-r--r--Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs123
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs3
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs22
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs22
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs323
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs66
19 files changed, 2487 insertions, 428 deletions
diff --git a/Emby.Server.Implementations/Data/ItemTypeLookup.cs b/Emby.Server.Implementations/Data/ItemTypeLookup.cs
index 1f73755f5..b66e7f5d9 100644
--- a/Emby.Server.Implementations/Data/ItemTypeLookup.cs
+++ b/Emby.Server.Implementations/Data/ItemTypeLookup.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading.Channels;
using Emby.Server.Implementations.Playlists;
using Jellyfin.Data.Enums;
+using Jellyfin.Server.Implementations;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
@@ -14,19 +15,13 @@ using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Data;
-/// <summary>
-/// Provides static topic based lookups for the BaseItemKind.
-/// </summary>
+/// <inheritdoc />
public class ItemTypeLookup : IItemTypeLookup
{
- /// <summary>
- /// Gets all values of the ItemFields type.
- /// </summary>
+ /// <inheritdoc />
public IReadOnlyList<ItemFields> AllItemFields { get; } = Enum.GetValues<ItemFields>();
- /// <summary>
- /// Gets all BaseItemKinds that are considered Programs.
- /// </summary>
+ /// <inheritdoc />
public IReadOnlyList<BaseItemKind> ProgramTypes { get; } =
[
BaseItemKind.Program,
@@ -35,9 +30,7 @@ public class ItemTypeLookup : IItemTypeLookup
BaseItemKind.LiveTvChannel
];
- /// <summary>
- /// Gets all BaseItemKinds that should be excluded from parent lookup.
- /// </summary>
+ /// <inheritdoc />
public IReadOnlyList<BaseItemKind> ProgramExcludeParentTypes { get; } =
[
BaseItemKind.Series,
@@ -47,27 +40,21 @@ public class ItemTypeLookup : IItemTypeLookup
BaseItemKind.PhotoAlbum
];
- /// <summary>
- /// Gets all BaseItemKinds that are considered to be provided by services.
- /// </summary>
+ /// <inheritdoc />
public IReadOnlyList<BaseItemKind> ServiceTypes { get; } =
[
BaseItemKind.TvChannel,
BaseItemKind.LiveTvChannel
];
- /// <summary>
- /// Gets all BaseItemKinds that have a StartDate.
- /// </summary>
+ /// <inheritdoc />
public IReadOnlyList<BaseItemKind> StartDateTypes { get; } =
[
BaseItemKind.Program,
BaseItemKind.LiveTvProgram
];
- /// <summary>
- /// Gets all BaseItemKinds that are considered Series.
- /// </summary>
+ /// <inheritdoc />
public IReadOnlyList<BaseItemKind> SeriesTypes { get; } =
[
BaseItemKind.Book,
@@ -76,9 +63,7 @@ public class ItemTypeLookup : IItemTypeLookup
BaseItemKind.Season
];
- /// <summary>
- /// Gets all BaseItemKinds that are not to be evaluated for Artists.
- /// </summary>
+ /// <inheritdoc />
public IReadOnlyList<BaseItemKind> ArtistExcludeParentTypes { get; } =
[
BaseItemKind.Series,
@@ -86,9 +71,7 @@ public class ItemTypeLookup : IItemTypeLookup
BaseItemKind.PhotoAlbum
];
- /// <summary>
- /// Gets all BaseItemKinds that are considered Artists.
- /// </summary>
+ /// <inheritdoc />
public IReadOnlyList<BaseItemKind> ArtistsTypes { get; } =
[
BaseItemKind.Audio,
@@ -97,9 +80,7 @@ public class ItemTypeLookup : IItemTypeLookup
BaseItemKind.AudioBook
];
- /// <summary>
- /// Gets mapping for all BaseItemKinds and their expected serialisaition target.
- /// </summary>
+ /// <inheritdoc />
public IDictionary<BaseItemKind, string?> BaseItemKindNames { get; } = new Dictionary<BaseItemKind, string?>()
{
{ BaseItemKind.AggregateFolder, typeof(AggregateFolder).FullName },
diff --git a/Jellyfin.Data/Entities/BaseItemEntity.cs b/Jellyfin.Data/Entities/BaseItemEntity.cs
index dbe5a5372..cd1991891 100644
--- a/Jellyfin.Data/Entities/BaseItemEntity.cs
+++ b/Jellyfin.Data/Entities/BaseItemEntity.cs
@@ -10,9 +10,7 @@ namespace Jellyfin.Data.Entities;
public class BaseItemEntity
{
- [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
-
- public Guid Id { get; set; }
+ public required Guid Id { get; set; }
public required string Type { get; set; }
@@ -78,12 +76,8 @@ public class BaseItemEntity
public bool IsInMixedFolder { get; set; }
- public string? LockedFields { get; set; }
-
public string? Studios { get; set; }
- public string? Audio { get; set; }
-
public string? ExternalServiceId { get; set; }
public string? Tags { get; set; }
@@ -94,8 +88,6 @@ public class BaseItemEntity
public string? UnratedType { get; set; }
- public string? TrailerTypes { get; set; }
-
public float? CriticRating { get; set; }
public string? CleanName { get; set; }
@@ -126,15 +118,13 @@ public class BaseItemEntity
public string? Tagline { get; set; }
- public string? Images { get; set; }
-
public string? ProductionLocations { get; set; }
public string? ExtraIds { get; set; }
public int? TotalBitrate { get; set; }
- public string? ExtraType { get; set; }
+ public BaseItemExtraType? ExtraType { get; set; }
public string? Artists { get; set; }
@@ -154,6 +144,8 @@ public class BaseItemEntity
public long? Size { get; set; }
+ public ProgramAudioEntity? Audio { get; set; }
+
public Guid? ParentId { get; set; }
public Guid? TopParentId { get; set; }
@@ -176,6 +168,12 @@ public class BaseItemEntity
public ICollection<AncestorId>? AncestorIds { get; set; }
+ public ICollection<BaseItemMetadataField>? LockedFields { get; set; }
+
+ public ICollection<BaseItemTrailerType>? TrailerTypes { get; set; }
+
+ public ICollection<BaseItemImageInfo>? Images { get; set; }
+
// those are references to __LOCAL__ ids not DB ids ... TODO: Bring the whole folder structure into the DB
// public ICollection<BaseItemEntity>? SeriesEpisodes { get; set; }
// public BaseItemEntity? Series { get; set; }
diff --git a/Jellyfin.Data/Entities/BaseItemExtraType.cs b/Jellyfin.Data/Entities/BaseItemExtraType.cs
new file mode 100644
index 000000000..341697436
--- /dev/null
+++ b/Jellyfin.Data/Entities/BaseItemExtraType.cs
@@ -0,0 +1,18 @@
+namespace Jellyfin.Data.Entities;
+
+#pragma warning disable CS1591
+public enum BaseItemExtraType
+{
+ Unknown = 0,
+ Clip = 1,
+ Trailer = 2,
+ BehindTheScenes = 3,
+ DeletedScene = 4,
+ Interview = 5,
+ Scene = 6,
+ Sample = 7,
+ ThemeSong = 8,
+ ThemeVideo = 9,
+ Featurette = 10,
+ Short = 11
+}
diff --git a/Jellyfin.Data/Entities/BaseItemImageInfo.cs b/Jellyfin.Data/Entities/BaseItemImageInfo.cs
new file mode 100644
index 000000000..6390cac58
--- /dev/null
+++ b/Jellyfin.Data/Entities/BaseItemImageInfo.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Data.Entities;
+#pragma warning disable CA2227
+
+/// <summary>
+/// Enum TrailerTypes.
+/// </summary>
+public class BaseItemImageInfo
+{
+ /// <summary>
+ /// Gets or Sets.
+ /// </summary>
+ public required Guid Id { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the path to the original image.
+ /// </summary>
+ public required string Path { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the time the image was last modified.
+ /// </summary>
+ public DateTime DateModified { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the imagetype.
+ /// </summary>
+ public ImageInfoImageType ImageType { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the width of the original image.
+ /// </summary>
+ public int Width { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the height of the original image.
+ /// </summary>
+ public int Height { get; set; }
+
+#pragma warning disable CA1819
+ /// <summary>
+ /// Gets or Sets the blurhash.
+ /// </summary>
+ public byte[]? Blurhash { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the reference id to the BaseItem.
+ /// </summary>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the referenced Item.
+ /// </summary>
+ public required BaseItemEntity Item { get; set; }
+}
diff --git a/Jellyfin.Data/Entities/BaseItemMetadataField.cs b/Jellyfin.Data/Entities/BaseItemMetadataField.cs
new file mode 100644
index 000000000..2f8e910f2
--- /dev/null
+++ b/Jellyfin.Data/Entities/BaseItemMetadataField.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Data.Entities;
+#pragma warning disable CA2227
+
+/// <summary>
+/// Enum MetadataFields.
+/// </summary>
+public class BaseItemMetadataField
+{
+ /// <summary>
+ /// Gets or Sets Numerical ID of this enumeratable.
+ /// </summary>
+ public required int Id { get; set; }
+
+ /// <summary>
+ /// Gets or Sets all referenced <see cref="BaseItemEntity"/>.
+ /// </summary>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets all referenced <see cref="BaseItemEntity"/>.
+ /// </summary>
+ public required BaseItemEntity Item { get; set; }
+}
diff --git a/Jellyfin.Data/Entities/BaseItemTrailerType.cs b/Jellyfin.Data/Entities/BaseItemTrailerType.cs
new file mode 100644
index 000000000..7dee20c87
--- /dev/null
+++ b/Jellyfin.Data/Entities/BaseItemTrailerType.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Data.Entities;
+#pragma warning disable CA2227
+/// <summary>
+/// Enum TrailerTypes.
+/// </summary>
+public class BaseItemTrailerType
+{
+ /// <summary>
+ /// Gets or Sets Numerical ID of this enumeratable.
+ /// </summary>
+ public required int Id { get; set; }
+
+ /// <summary>
+ /// Gets or Sets all referenced <see cref="BaseItemEntity"/>.
+ /// </summary>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets all referenced <see cref="BaseItemEntity"/>.
+ /// </summary>
+ public required BaseItemEntity Item { get; set; }
+}
diff --git a/Jellyfin.Data/Entities/EnumLikeTable.cs b/Jellyfin.Data/Entities/EnumLikeTable.cs
new file mode 100644
index 000000000..11e1d0aa9
--- /dev/null
+++ b/Jellyfin.Data/Entities/EnumLikeTable.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Defines an Entity that is modeled after an Enum.
+/// </summary>
+public abstract class EnumLikeTable
+{
+ /// <summary>
+ /// Gets or Sets Numerical ID of this enumeratable.
+ /// </summary>
+ public required int Id { get; set; }
+}
diff --git a/Jellyfin.Data/Entities/ImageInfoImageType.cs b/Jellyfin.Data/Entities/ImageInfoImageType.cs
new file mode 100644
index 000000000..f78178dd2
--- /dev/null
+++ b/Jellyfin.Data/Entities/ImageInfoImageType.cs
@@ -0,0 +1,76 @@
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Enum ImageType.
+/// </summary>
+public enum ImageInfoImageType
+{
+ /// <summary>
+ /// The primary.
+ /// </summary>
+ Primary = 0,
+
+ /// <summary>
+ /// The art.
+ /// </summary>
+ Art = 1,
+
+ /// <summary>
+ /// The backdrop.
+ /// </summary>
+ Backdrop = 2,
+
+ /// <summary>
+ /// The banner.
+ /// </summary>
+ Banner = 3,
+
+ /// <summary>
+ /// The logo.
+ /// </summary>
+ Logo = 4,
+
+ /// <summary>
+ /// The thumb.
+ /// </summary>
+ Thumb = 5,
+
+ /// <summary>
+ /// The disc.
+ /// </summary>
+ Disc = 6,
+
+ /// <summary>
+ /// The box.
+ /// </summary>
+ Box = 7,
+
+ /// <summary>
+ /// The screenshot.
+ /// </summary>
+ /// <remarks>
+ /// This enum value is obsolete.
+ /// XmlSerializer does not serialize/deserialize objects that are marked as [Obsolete].
+ /// </remarks>
+ Screenshot = 8,
+
+ /// <summary>
+ /// The menu.
+ /// </summary>
+ Menu = 9,
+
+ /// <summary>
+ /// The chapter image.
+ /// </summary>
+ Chapter = 10,
+
+ /// <summary>
+ /// The box rear.
+ /// </summary>
+ BoxRear = 11,
+
+ /// <summary>
+ /// The user profile image.
+ /// </summary>
+ Profile = 12
+}
diff --git a/Jellyfin.Data/Entities/ProgramAudioEntity.cs b/Jellyfin.Data/Entities/ProgramAudioEntity.cs
new file mode 100644
index 000000000..fafccb13c
--- /dev/null
+++ b/Jellyfin.Data/Entities/ProgramAudioEntity.cs
@@ -0,0 +1,37 @@
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Lists types of Audio.
+/// </summary>
+public enum ProgramAudioEntity
+{
+ /// <summary>
+ /// Mono.
+ /// </summary>
+ Mono,
+
+ /// <summary>
+ /// Sterio.
+ /// </summary>
+ Stereo,
+
+ /// <summary>
+ /// Dolby.
+ /// </summary>
+ Dolby,
+
+ /// <summary>
+ /// DolbyDigital.
+ /// </summary>
+ DolbyDigital,
+
+ /// <summary>
+ /// Thx.
+ /// </summary>
+ Thx,
+
+ /// <summary>
+ /// Atmos.
+ /// </summary>
+ Atmos
+}
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
index 480d83eb1..6ddab9e3d 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -7,6 +7,7 @@ using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading;
+using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller;
@@ -69,6 +70,8 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
context.AncestorIds.Where(e => e.ItemId == id).ExecuteDelete();
context.ItemValues.Where(e => e.ItemId == id).ExecuteDelete();
context.BaseItems.Where(e => e.Id == id).ExecuteDelete();
+ context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete();
+ context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete();
context.SaveChanges();
transaction.Commit();
}
@@ -229,7 +232,12 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
var result = new QueryResult<BaseItemDto>();
using var context = dbProvider.CreateDbContext();
- var dbQuery = TranslateQuery(context.BaseItems, context, filter)
+ IQueryable<BaseItemEntity> dbQuery = context.BaseItems
+ .Include(e => e.ExtraType)
+ .Include(e => e.TrailerTypes)
+ .Include(e => e.Images)
+ .Include(e => e.LockedFields);
+ dbQuery = TranslateQuery(dbQuery, context, filter)
.DistinctBy(e => e.Id);
if (filter.EnableTotalRecordCount)
{
@@ -585,8 +593,8 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
if (filter.TrailerTypes.Length > 0)
{
- var trailerTypes = filter.TrailerTypes.Select(e => e.ToString()).ToArray();
- baseQuery = baseQuery.Where(e => trailerTypes.Any(f => e.TrailerTypes!.Contains(f)));
+ var trailerTypes = filter.TrailerTypes.Select(e => (int)e).ToArray();
+ baseQuery = baseQuery.Where(e => trailerTypes.Any(f => e.TrailerTypes!.Any(w => w.Id == f)));
}
if (filter.IsAiring.HasValue)
@@ -666,8 +674,8 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
if (filter.ImageTypes.Length > 0)
{
- var imgTypes = filter.ImageTypes.Select(e => e.ToString()).ToArray();
- baseQuery = baseQuery.Where(e => imgTypes.Any(f => e.Images!.Contains(f)));
+ var imgTypes = filter.ImageTypes.Select(e => (ImageInfoImageType)e).ToArray();
+ baseQuery = baseQuery.Where(e => imgTypes.Any(f => e.Images!.Any(w => w.ImageType == f)));
}
if (filter.IsLiked.HasValue)
@@ -1206,12 +1214,12 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
{
ArgumentNullException.ThrowIfNull(item);
- var images = SerializeImages(item.ImageInfos);
+ var images = item.ImageInfos.Select(e => Map(item.Id, e));
using var db = dbProvider.CreateDbContext();
-
- db.BaseItems
- .Where(e => e.Id == item.Id)
- .ExecuteUpdate(e => e.SetProperty(f => f.Images, images));
+ using var transaction = db.Database.BeginTransaction();
+ db.BaseItemImageInfos.Where(e => e.ItemId == item.Id).ExecuteDelete();
+ db.BaseItemImageInfos.AddRange(images);
+ transaction.Commit();
}
/// <inheritdoc cref="IItemRepository" />
@@ -1260,29 +1268,32 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
context.AncestorIds.Where(e => e.ItemId == entity.Id).ExecuteDelete();
if (item.Item.SupportsAncestors && item.AncestorIds != null)
{
+ entity.AncestorIds = new List<AncestorId>();
foreach (var ancestorId in item.AncestorIds)
{
- context.AncestorIds.Add(new Data.Entities.AncestorId()
+ entity.AncestorIds.Add(new AncestorId()
{
Item = entity,
AncestorIdText = ancestorId.ToString(),
Id = ancestorId,
- ItemId = Guid.Empty
+ ItemId = entity.Id
});
}
}
var itemValues = GetItemValuesToSave(item.Item, item.InheritedTags);
context.ItemValues.Where(e => e.ItemId == entity.Id).ExecuteDelete();
+ entity.ItemValues = new List<ItemValue>();
+
foreach (var itemValue in itemValues)
{
- context.ItemValues.Add(new()
+ entity.ItemValues.Add(new()
{
Item = entity,
Type = itemValue.MagicNumber,
Value = itemValue.Value,
CleanValue = GetCleanValue(itemValue.Value),
- ItemId = Guid.Empty
+ ItemId = entity.Id
});
}
}
@@ -1366,26 +1377,17 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
if (entity.ExtraType is not null)
{
- dto.ExtraType = Enum.Parse<ExtraType>(entity.ExtraType);
+ dto.ExtraType = (ExtraType)entity.ExtraType;
}
if (entity.LockedFields is not null)
{
- List<MetadataField>? fields = null;
- foreach (var i in entity.LockedFields.AsSpan().Split('|'))
- {
- if (Enum.TryParse(i, true, out MetadataField parsedValue))
- {
- (fields ??= new List<MetadataField>()).Add(parsedValue);
- }
- }
-
- dto.LockedFields = fields?.ToArray() ?? Array.Empty<MetadataField>();
+ dto.LockedFields = entity.LockedFields?.Select(e => (MetadataField)e.Id).ToArray() ?? [];
}
if (entity.Audio is not null)
{
- dto.Audio = Enum.Parse<ProgramAudio>(entity.Audio);
+ dto.Audio = (ProgramAudio)entity.Audio;
}
dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? null : entity.ExtraIds.Split('|').Select(e => Guid.Parse(e)).ToArray();
@@ -1408,16 +1410,7 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
if (dto is Trailer trailer)
{
- List<TrailerType>? types = null;
- foreach (var i in entity.TrailerTypes.AsSpan().Split('|'))
- {
- if (Enum.TryParse(i, true, out TrailerType parsedValue))
- {
- (types ??= new List<TrailerType>()).Add(parsedValue);
- }
- }
-
- trailer.TrailerTypes = types?.ToArray() ?? Array.Empty<TrailerType>();
+ trailer.TrailerTypes = entity.TrailerTypes?.Select(e => (TrailerType)e.Id).ToArray() ?? [];
}
if (dto is Video video)
@@ -1455,7 +1448,7 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
if (entity.Images is not null)
{
- dto.ImageInfos = DeserializeImages(entity.Images);
+ dto.ImageInfos = entity.Images.Select(Map).ToArray();
}
// dto.Type = entity.Type;
@@ -1490,8 +1483,8 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
var entity = new BaseItemEntity()
{
Type = dto.GetType().ToString(),
+ Id = dto.Id
};
- entity.Id = dto.Id;
entity.ParentId = dto.ParentId;
entity.Path = GetPathToSave(dto.Path);
entity.EndDate = dto.EndDate.GetValueOrDefault();
@@ -1533,21 +1526,35 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
entity.OwnerId = dto.OwnerId.ToString();
entity.Width = dto.Width;
entity.Height = dto.Height;
- entity.Provider = dto.ProviderIds.Select(e => new Data.Entities.BaseItemProvider()
+ entity.Provider = dto.ProviderIds.Select(e => new BaseItemProvider()
{
Item = entity,
ProviderId = e.Key,
ProviderValue = e.Value
}).ToList();
- entity.Audio = dto.Audio?.ToString();
- entity.ExtraType = dto.ExtraType?.ToString();
+ if (dto.Audio.HasValue)
+ {
+ entity.Audio = (ProgramAudioEntity)dto.Audio;
+ }
+
+ if (dto.ExtraType.HasValue)
+ {
+ entity.ExtraType = (BaseItemExtraType)dto.ExtraType;
+ }
entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations) : null;
entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
- entity.LockedFields = dto.LockedFields is not null ? string.Join('|', dto.LockedFields) : null;
+ entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
+ .Select(e => new BaseItemMetadataField()
+ {
+ Id = (int)e,
+ Item = entity,
+ ItemId = entity.Id
+ })
+ .ToArray() : null;
if (dto is IHasProgramAttributes hasProgramAttributes)
{
@@ -1562,11 +1569,6 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
entity.ExternalServiceId = liveTvChannel.ServiceName;
}
- if (dto is Trailer trailer)
- {
- entity.LockedFields = trailer.LockedFields is not null ? string.Join('|', trailer.LockedFields) : null;
- }
-
if (dto is Video video)
{
entity.PrimaryVersionId = video.PrimaryVersionId;
@@ -1602,7 +1604,17 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
if (dto.ImageInfos is not null)
{
- entity.Images = SerializeImages(dto.ImageInfos);
+ entity.Images = dto.ImageInfos.Select(f => Map(dto.Id, f)).ToArray();
+ }
+
+ if (dto is Trailer trailer)
+ {
+ entity.TrailerTypes = trailer.TrailerTypes?.Select(e => new BaseItemTrailerType()
+ {
+ Id = (int)e,
+ Item = entity,
+ ItemId = entity.Id
+ }).ToArray() ?? [];
}
// dto.Type = entity.Type;
@@ -1863,90 +1875,33 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
}
}
- internal string? SerializeImages(ItemImageInfo[] images)
- {
- if (images.Length == 0)
- {
- return null;
- }
-
- StringBuilder str = new StringBuilder();
- foreach (var i in images)
- {
- if (string.IsNullOrWhiteSpace(i.Path))
- {
- continue;
- }
-
- AppendItemImageInfo(str, i);
- str.Append('|');
- }
-
- str.Length -= 1; // Remove last |
- return str.ToString();
- }
-
- internal ItemImageInfo[] DeserializeImages(string value)
+ private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
{
- 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];
+ 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!
+ };
}
- private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
+ private static ItemImageInfo Map(BaseItemImageInfo e)
{
- const char Delimiter = '*';
-
- var path = image.Path ?? string.Empty;
-
- bldr.Append(GetPathToSave(path))
- .Append(Delimiter)
- .Append(image.DateModified.Ticks)
- .Append(Delimiter)
- .Append(image.Type)
- .Append(Delimiter)
- .Append(image.Width)
- .Append(Delimiter)
- .Append(image.Height);
-
- var hash = image.BlurHash;
- if (!string.IsNullOrEmpty(hash))
- {
- bldr.Append(Delimiter)
- // Replace delimiters with other characters.
- // This can be removed when we migrate to a proper DB.
- .Append(hash.Replace(Delimiter, '/').Replace('|', '\\'));
- }
+ return new ItemImageInfo()
+ {
+ Path = e.Path,
+ BlurHash = e.Blurhash != null ? Encoding.UTF8.GetString(e.Blurhash) : null,
+ DateModified = e.DateModified,
+ Height = e.Height,
+ Width = e.Width,
+ Type = (ImageType)e.ImageType
+ };
}
private string? GetPathToSave(string path)
@@ -1964,111 +1919,6 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
return appHost.ExpandVirtualPath(path);
}
- 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 = RestorePath(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 List<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
{
var list = new List<string>();
diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
index a9eda1b64..406230a70 100644
--- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
@@ -131,6 +131,21 @@ public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILog
/// </summary>
public DbSet<BaseItemProvider> BaseItemProviders => Set<BaseItemProvider>();
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/>.
+ /// </summary>
+ public DbSet<BaseItemImageInfo> BaseItemImageInfos => Set<BaseItemImageInfo>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/>.
+ /// </summary>
+ public DbSet<BaseItemMetadataField> BaseItemMetadataFields => Set<BaseItemMetadataField>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/>.
+ /// </summary>
+ public DbSet<BaseItemTrailerType> BaseItemTrailerTypes => Set<BaseItemTrailerType>();
+
/*public DbSet<Artwork> Artwork => Set<Artwork>();
public DbSet<Book> Books => Set<Book>();
diff --git a/Jellyfin.Server.Implementations/Migrations/20241009225800_ExpandedBaseItemFields.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20241009225800_ExpandedBaseItemFields.Designer.cs
new file mode 100644
index 000000000..7f69e8448
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20241009225800_ExpandedBaseItemFields.Designer.cs
@@ -0,0 +1,1540 @@
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20241009225800_ExpandedBaseItemFields")]
+ partial class ExpandedBaseItemFields
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.10");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("Id")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AncestorIdText")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "Id");
+
+ b.HasIndex("Id");
+
+ b.HasIndex("ItemId", "AncestorIdText");
+
+ b.ToTable("AncestorIds");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Filename")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("MimeType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "Index");
+
+ b.ToTable("AttachmentStreamInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Album")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AlbumArtists")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Artists")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Audio")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanName")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("CommunityRating")
+ .HasColumnType("REAL");
+
+ b.Property<float?>("CriticRating")
+ .HasColumnType("REAL");
+
+ b.Property<string>("CustomRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Data")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastMediaAdded")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastRefreshed")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastSaved")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("EndDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("EpisodeTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalSeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalServiceId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraIds")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ExtraType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ForcedSortName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Genres")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("InheritedParentalRatingValue")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsInMixedFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsMovie")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsRepeat")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsSeries")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsVirtualItem")
+ .HasColumnType("INTEGER");
+
+ b.Property<float?>("LUFS")
+ .HasColumnType("REAL");
+
+ b.Property<string>("MediaType")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("NormalizationGain")
+ .HasColumnType("REAL");
+
+ b.Property<string>("OfficialRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OwnerId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ParentIndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataCountryCode")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("PremiereDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PrimaryVersionId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProductionLocations")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ProductionYear")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("RunTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("SeasonId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeasonName")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("SeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesPresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ShowId")
+ .HasColumnType("TEXT");
+
+ b.Property<long?>("Size")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("StartDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Studios")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tagline")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tags")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("TopParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("TotalBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UnratedType")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UserDataKey")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ParentId");
+
+ b.HasIndex("Path");
+
+ b.HasIndex("PresentationUniqueKey");
+
+ b.HasIndex("TopParentId", "Id");
+
+ b.HasIndex("UserDataKey", "Type");
+
+ b.HasIndex("Type", "TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "TopParentId", "StartDate");
+
+ b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName");
+
+ b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.ToTable("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<byte[]>("Blurhash")
+ .HasColumnType("BLOB");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ImageType")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemMetadataFields");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ProviderId");
+
+ b.HasIndex("ProviderId", "ProviderValue", "ItemId");
+
+ b.ToTable("BaseItemProviders");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemTrailerTypes");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ChapterIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("ImageDateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ImagePath")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "ChapterIndex");
+
+ b.ToTable("Chapters");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "Type", "Value");
+
+ b.HasIndex("ItemId", "Type", "CleanValue");
+
+ b.ToTable("ItemValues");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("EndTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SegmentProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaSegments");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("StreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AspectRatio")
+ .HasColumnType("TEXT");
+
+ b.Property<float>("AverageFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int>("BitDepth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("BitRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("BlPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelLayout")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Channels")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTimeBase")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorPrimaries")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorSpace")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorTransfer")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("DvBlSignalCompatibilityId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DvLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DvProfile")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DvVersionMajor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DvVersionMinor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ElPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsAnamorphic")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsAvc")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsDefault")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsExternal")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsForced")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsHearingImpaired")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsInterlaced")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("KeyFrames")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Language")
+ .HasColumnType("TEXT");
+
+ b.Property<float>("Level")
+ .HasColumnType("REAL");
+
+ b.Property<string>("NalLengthSize")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PixelFormat")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Profile")
+ .HasColumnType("TEXT");
+
+ b.Property<float>("RealFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int>("RefFrames")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Rotation")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("RpuPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SampleRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("StreamType")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("TimeBase")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Title")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "StreamIndex");
+
+ b.HasIndex("StreamIndex");
+
+ b.HasIndex("StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType", "Language");
+
+ b.ToTable("MediaStreamInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Role")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ListOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PersonType")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Role", "ListOrder");
+
+ b.HasIndex("Name");
+
+ b.HasIndex("ItemId", "ListOrder");
+
+ b.ToTable("Peoples");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ {
+ b.Property<string>("Key")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("AudioStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("BaseItemEntityId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsFavorite")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastPlayedDate")
+ .HasColumnType("TEXT");
+
+ b.Property<bool?>("Likes")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("PlayCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("PlaybackPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Played")
+ .HasColumnType("INTEGER");
+
+ b.Property<double?>("Rating")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("SubtitleStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Key", "UserId");
+
+ b.HasIndex("BaseItemEntityId");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("Key", "UserId", "IsFavorite");
+
+ b.HasIndex("Key", "UserId", "LastPlayedDate");
+
+ b.HasIndex("Key", "UserId", "PlaybackPositionTicks");
+
+ b.HasIndex("Key", "UserId", "Played");
+
+ b.ToTable("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("AncestorIds")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", null)
+ .WithMany("UserData")
+ .HasForeignKey("BaseItemEntityId");
+
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ {
+ b.Navigation("AncestorIds");
+
+ b.Navigation("Chapters");
+
+ b.Navigation("Images");
+
+ b.Navigation("ItemValues");
+
+ b.Navigation("LockedFields");
+
+ b.Navigation("MediaStreams");
+
+ b.Navigation("Peoples");
+
+ b.Navigation("Provider");
+
+ b.Navigation("TrailerTypes");
+
+ b.Navigation("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20241009225800_ExpandedBaseItemFields.cs b/Jellyfin.Server.Implementations/Migrations/20241009225800_ExpandedBaseItemFields.cs
new file mode 100644
index 000000000..f1238db82
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20241009225800_ExpandedBaseItemFields.cs
@@ -0,0 +1,169 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class ExpandedBaseItemFields : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "Images",
+ table: "BaseItems");
+
+ migrationBuilder.DropColumn(
+ name: "LockedFields",
+ table: "BaseItems");
+
+ migrationBuilder.DropColumn(
+ name: "TrailerTypes",
+ table: "BaseItems");
+
+ migrationBuilder.AlterColumn<int>(
+ name: "ExtraType",
+ table: "BaseItems",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<int>(
+ name: "Audio",
+ table: "BaseItems",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.CreateTable(
+ name: "BaseItemImageInfos",
+ columns: table => new
+ {
+ Id = table.Column<Guid>(type: "TEXT", nullable: false),
+ Path = table.Column<string>(type: "TEXT", nullable: false),
+ DateModified = table.Column<DateTime>(type: "TEXT", nullable: false),
+ ImageType = table.Column<int>(type: "INTEGER", nullable: false),
+ Width = table.Column<int>(type: "INTEGER", nullable: false),
+ Height = table.Column<int>(type: "INTEGER", nullable: false),
+ Blurhash = table.Column<byte[]>(type: "BLOB", nullable: true),
+ ItemId = table.Column<Guid>(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_BaseItemImageInfos", x => x.Id);
+ table.ForeignKey(
+ name: "FK_BaseItemImageInfos_BaseItems_ItemId",
+ column: x => x.ItemId,
+ principalTable: "BaseItems",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "BaseItemMetadataFields",
+ columns: table => new
+ {
+ Id = table.Column<int>(type: "INTEGER", nullable: false),
+ ItemId = table.Column<Guid>(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_BaseItemMetadataFields", x => new { x.Id, x.ItemId });
+ table.ForeignKey(
+ name: "FK_BaseItemMetadataFields_BaseItems_ItemId",
+ column: x => x.ItemId,
+ principalTable: "BaseItems",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "BaseItemTrailerTypes",
+ columns: table => new
+ {
+ Id = table.Column<int>(type: "INTEGER", nullable: false),
+ ItemId = table.Column<Guid>(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_BaseItemTrailerTypes", x => new { x.Id, x.ItemId });
+ table.ForeignKey(
+ name: "FK_BaseItemTrailerTypes_BaseItems_ItemId",
+ column: x => x.ItemId,
+ principalTable: "BaseItems",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BaseItemImageInfos_ItemId",
+ table: "BaseItemImageInfos",
+ column: "ItemId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BaseItemMetadataFields_ItemId",
+ table: "BaseItemMetadataFields",
+ column: "ItemId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BaseItemTrailerTypes_ItemId",
+ table: "BaseItemTrailerTypes",
+ column: "ItemId");
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "BaseItemImageInfos");
+
+ migrationBuilder.DropTable(
+ name: "BaseItemMetadataFields");
+
+ migrationBuilder.DropTable(
+ name: "BaseItemTrailerTypes");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "ExtraType",
+ table: "BaseItems",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<string>(
+ name: "Audio",
+ table: "BaseItems",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AddColumn<string>(
+ name: "Images",
+ table: "BaseItems",
+ type: "TEXT",
+ nullable: true);
+
+ migrationBuilder.AddColumn<string>(
+ name: "LockedFields",
+ table: "BaseItems",
+ type: "TEXT",
+ nullable: true);
+
+ migrationBuilder.AddColumn<string>(
+ name: "TrailerTypes",
+ table: "BaseItems",
+ type: "TEXT",
+ nullable: true);
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index dd280489b..1a3a5910f 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "8.0.8");
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.10");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@@ -154,8 +154,8 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<string>("Artists")
.HasColumnType("TEXT");
- b.Property<string>("Audio")
- .HasColumnType("TEXT");
+ b.Property<int?>("Audio")
+ .HasColumnType("INTEGER");
b.Property<string>("ChannelId")
.HasColumnType("TEXT");
@@ -208,8 +208,8 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<string>("ExtraIds")
.HasColumnType("TEXT");
- b.Property<string>("ExtraType")
- .HasColumnType("TEXT");
+ b.Property<int?>("ExtraType")
+ .HasColumnType("INTEGER");
b.Property<string>("ForcedSortName")
.HasColumnType("TEXT");
@@ -220,9 +220,6 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<int?>("Height")
.HasColumnType("INTEGER");
- b.Property<string>("Images")
- .HasColumnType("TEXT");
-
b.Property<int?>("IndexNumber")
.HasColumnType("INTEGER");
@@ -253,9 +250,6 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<float?>("LUFS")
.HasColumnType("REAL");
- b.Property<string>("LockedFields")
- .HasColumnType("TEXT");
-
b.Property<string>("MediaType")
.HasColumnType("TEXT");
@@ -352,9 +346,6 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<int?>("TotalBitrate")
.HasColumnType("INTEGER");
- b.Property<string>("TrailerTypes")
- .HasColumnType("TEXT");
-
b.Property<string>("Type")
.IsRequired()
.HasColumnType("TEXT");
@@ -401,6 +392,56 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("BaseItems");
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<byte[]>("Blurhash")
+ .HasColumnType("BLOB");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ImageType")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemMetadataFields");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
{
b.Property<Guid>("ItemId")
@@ -420,6 +461,21 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("BaseItemProviders");
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemTrailerTypes");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
{
b.Property<Guid>("ItemId")
@@ -1268,6 +1324,28 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("Item");
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
{
b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
@@ -1279,6 +1357,17 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("Item");
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
{
b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
@@ -1406,14 +1495,20 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("Chapters");
+ b.Navigation("Images");
+
b.Navigation("ItemValues");
+ b.Navigation("LockedFields");
+
b.Navigation("MediaStreams");
b.Navigation("Peoples");
b.Navigation("Provider");
+ b.Navigation("TrailerTypes");
+
b.Navigation("UserData");
});
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs
index 6c36a1591..ab5403271 100644
--- a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs
@@ -27,6 +27,9 @@ public class BaseItemConfiguration : IEntityTypeConfiguration<BaseItemEntity>
builder.HasMany(e => e.Chapters);
builder.HasMany(e => e.Provider);
builder.HasMany(e => e.AncestorIds);
+ builder.HasMany(e => e.LockedFields);
+ builder.HasMany(e => e.TrailerTypes);
+ builder.HasMany(e => e.Images);
builder.HasIndex(e => e.Path);
builder.HasIndex(e => e.ParentId);
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs
new file mode 100644
index 000000000..137f4a883
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Linq;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Model.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using SQLitePCL;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// Provides configuration for the BaseItemMetadataField entity.
+/// </summary>
+public class BaseItemMetadataFieldConfiguration : IEntityTypeConfiguration<BaseItemMetadataField>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<BaseItemMetadataField> builder)
+ {
+ builder.HasKey(e => new { e.Id, e.ItemId });
+ builder.HasOne(e => e.Item);
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs
new file mode 100644
index 000000000..f03d99c29
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Linq;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Model.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using SQLitePCL;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// Provides configuration for the BaseItemMetadataField entity.
+/// </summary>
+public class BaseItemTrailerTypeConfiguration : IEntityTypeConfiguration<BaseItemTrailerType>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<BaseItemTrailerType> builder)
+ {
+ builder.HasKey(e => new { e.Id, e.ItemId });
+ builder.HasOne(e => e.Item);
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
index c4a15c64e..8ce423298 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
@@ -2,13 +2,17 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
using System.IO;
using System.Linq;
+using System.Text;
using Emby.Server.Implementations.Data;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Entities.Libraries;
+using Jellyfin.Extensions;
using Jellyfin.Server.Implementations;
using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
using Microsoft.Data.Sqlite;
@@ -503,293 +507,308 @@ public class MigrateLibraryDb : IMigrationRoutine
private BaseItemEntity GetItem(SqliteDataReader reader)
{
- var item = new BaseItemEntity()
+ var entity = new BaseItemEntity()
{
- Type = reader.GetString(0)
+ Type = reader.GetString(0),
+ Id = Guid.NewGuid()
};
var index = 1;
if (reader.TryGetString(index++, out var data))
{
- item.Data = data;
+ entity.Data = data;
}
if (reader.TryReadDateTime(index++, out var startDate))
{
- item.StartDate = startDate;
+ entity.StartDate = startDate;
}
if (reader.TryReadDateTime(index++, out var endDate))
{
- item.EndDate = endDate;
+ entity.EndDate = endDate;
}
if (reader.TryGetGuid(index++, out var guid))
{
- item.ChannelId = guid.ToString("N");
+ entity.ChannelId = guid.ToString("N");
}
if (reader.TryGetBoolean(index++, out var isMovie))
{
- item.IsMovie = isMovie;
+ entity.IsMovie = isMovie;
}
if (reader.TryGetBoolean(index++, out var isSeries))
{
- item.IsSeries = isSeries;
+ entity.IsSeries = isSeries;
}
if (reader.TryGetString(index++, out var episodeTitle))
{
- item.EpisodeTitle = episodeTitle;
+ entity.EpisodeTitle = episodeTitle;
}
if (reader.TryGetBoolean(index++, out var isRepeat))
{
- item.IsRepeat = isRepeat;
+ entity.IsRepeat = isRepeat;
}
if (reader.TryGetSingle(index++, out var communityRating))
{
- item.CommunityRating = communityRating;
+ entity.CommunityRating = communityRating;
}
if (reader.TryGetString(index++, out var customRating))
{
- item.CustomRating = customRating;
+ entity.CustomRating = customRating;
}
if (reader.TryGetInt32(index++, out var indexNumber))
{
- item.IndexNumber = indexNumber;
+ entity.IndexNumber = indexNumber;
}
if (reader.TryGetBoolean(index++, out var isLocked))
{
- item.IsLocked = isLocked;
+ entity.IsLocked = isLocked;
}
if (reader.TryGetString(index++, out var preferredMetadataLanguage))
{
- item.PreferredMetadataLanguage = preferredMetadataLanguage;
+ entity.PreferredMetadataLanguage = preferredMetadataLanguage;
}
if (reader.TryGetString(index++, out var preferredMetadataCountryCode))
{
- item.PreferredMetadataCountryCode = preferredMetadataCountryCode;
+ entity.PreferredMetadataCountryCode = preferredMetadataCountryCode;
}
if (reader.TryGetInt32(index++, out var width))
{
- item.Width = width;
+ entity.Width = width;
}
if (reader.TryGetInt32(index++, out var height))
{
- item.Height = height;
+ entity.Height = height;
}
if (reader.TryReadDateTime(index++, out var dateLastRefreshed))
{
- item.DateLastRefreshed = dateLastRefreshed;
+ entity.DateLastRefreshed = dateLastRefreshed;
}
if (reader.TryGetString(index++, out var name))
{
- item.Name = name;
+ entity.Name = name;
}
if (reader.TryGetString(index++, out var restorePath))
{
- item.Path = restorePath;
+ entity.Path = restorePath;
}
if (reader.TryReadDateTime(index++, out var premiereDate))
{
- item.PremiereDate = premiereDate;
+ entity.PremiereDate = premiereDate;
}
if (reader.TryGetString(index++, out var overview))
{
- item.Overview = overview;
+ entity.Overview = overview;
}
if (reader.TryGetInt32(index++, out var parentIndexNumber))
{
- item.ParentIndexNumber = parentIndexNumber;
+ entity.ParentIndexNumber = parentIndexNumber;
}
if (reader.TryGetInt32(index++, out var productionYear))
{
- item.ProductionYear = productionYear;
+ entity.ProductionYear = productionYear;
}
if (reader.TryGetString(index++, out var officialRating))
{
- item.OfficialRating = officialRating;
+ entity.OfficialRating = officialRating;
}
if (reader.TryGetString(index++, out var forcedSortName))
{
- item.ForcedSortName = forcedSortName;
+ entity.ForcedSortName = forcedSortName;
}
if (reader.TryGetInt64(index++, out var runTimeTicks))
{
- item.RunTimeTicks = runTimeTicks;
+ entity.RunTimeTicks = runTimeTicks;
}
if (reader.TryGetInt64(index++, out var size))
{
- item.Size = size;
+ entity.Size = size;
}
if (reader.TryReadDateTime(index++, out var dateCreated))
{
- item.DateCreated = dateCreated;
+ entity.DateCreated = dateCreated;
}
if (reader.TryReadDateTime(index++, out var dateModified))
{
- item.DateModified = dateModified;
+ entity.DateModified = dateModified;
}
- item.Id = reader.GetGuid(index++);
+ entity.Id = reader.GetGuid(index++);
if (reader.TryGetString(index++, out var genres))
{
- item.Genres = genres;
+ entity.Genres = genres;
}
if (reader.TryGetGuid(index++, out var parentId))
{
- item.ParentId = parentId;
+ entity.ParentId = parentId;
}
- if (reader.TryGetString(index++, out var audioString))
+ if (reader.TryGetString(index++, out var audioString) && Enum.TryParse<ProgramAudioEntity>(audioString, out var audioType))
{
- item.Audio = audioString;
+ entity.Audio = audioType;
}
if (reader.TryGetString(index++, out var serviceName))
{
- item.ExternalServiceId = serviceName;
+ entity.ExternalServiceId = serviceName;
}
if (reader.TryGetBoolean(index++, out var isInMixedFolder))
{
- item.IsInMixedFolder = isInMixedFolder;
+ entity.IsInMixedFolder = isInMixedFolder;
}
if (reader.TryReadDateTime(index++, out var dateLastSaved))
{
- item.DateLastSaved = dateLastSaved;
+ entity.DateLastSaved = dateLastSaved;
}
if (reader.TryGetString(index++, out var lockedFields))
{
- item.LockedFields = 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))
{
- item.Studios = studios;
+ entity.Studios = studios;
}
if (reader.TryGetString(index++, out var tags))
{
- item.Tags = tags;
+ entity.Tags = tags;
}
if (reader.TryGetString(index++, out var trailerTypes))
{
- item.TrailerTypes = 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))
{
- item.OriginalTitle = originalTitle;
+ entity.OriginalTitle = originalTitle;
}
if (reader.TryGetString(index++, out var primaryVersionId))
{
- item.PrimaryVersionId = primaryVersionId;
+ entity.PrimaryVersionId = primaryVersionId;
}
if (reader.TryReadDateTime(index++, out var dateLastMediaAdded))
{
- item.DateLastMediaAdded = dateLastMediaAdded;
+ entity.DateLastMediaAdded = dateLastMediaAdded;
}
if (reader.TryGetString(index++, out var album))
{
- item.Album = album;
+ entity.Album = album;
}
if (reader.TryGetSingle(index++, out var lUFS))
{
- item.LUFS = lUFS;
+ entity.LUFS = lUFS;
}
if (reader.TryGetSingle(index++, out var normalizationGain))
{
- item.NormalizationGain = normalizationGain;
+ entity.NormalizationGain = normalizationGain;
}
if (reader.TryGetSingle(index++, out var criticRating))
{
- item.CriticRating = criticRating;
+ entity.CriticRating = criticRating;
}
if (reader.TryGetBoolean(index++, out var isVirtualItem))
{
- item.IsVirtualItem = isVirtualItem;
+ entity.IsVirtualItem = isVirtualItem;
}
if (reader.TryGetString(index++, out var seriesName))
{
- item.SeriesName = seriesName;
+ entity.SeriesName = seriesName;
}
if (reader.TryGetString(index++, out var seasonName))
{
- item.SeasonName = seasonName;
+ entity.SeasonName = seasonName;
}
if (reader.TryGetGuid(index++, out var seasonId))
{
- item.SeasonId = seasonId;
+ entity.SeasonId = seasonId;
}
if (reader.TryGetGuid(index++, out var seriesId))
{
- item.SeriesId = seriesId;
+ entity.SeriesId = seriesId;
}
if (reader.TryGetString(index++, out var presentationUniqueKey))
{
- item.PresentationUniqueKey = presentationUniqueKey;
+ entity.PresentationUniqueKey = presentationUniqueKey;
}
if (reader.TryGetInt32(index++, out var parentalRating))
{
- item.InheritedParentalRatingValue = parentalRating;
+ entity.InheritedParentalRatingValue = parentalRating;
}
if (reader.TryGetString(index++, out var externalSeriesId))
{
- item.ExternalSeriesId = externalSeriesId;
+ entity.ExternalSeriesId = externalSeriesId;
}
if (reader.TryGetString(index++, out var tagLine))
{
- item.Tagline = tagLine;
+ entity.Tagline = tagLine;
}
if (reader.TryGetString(index++, out var providerIds))
{
- item.Provider = providerIds.Split('|').Select(e => e.Split("="))
+ entity.Provider = providerIds.Split('|').Select(e => e.Split("="))
.Select(e => new BaseItemProvider()
{
Item = null!,
@@ -800,59 +819,217 @@ public class MigrateLibraryDb : IMigrationRoutine
if (reader.TryGetString(index++, out var imageInfos))
{
- item.Images = imageInfos;
+ entity.Images = DeserializeImages(imageInfos).Select(f => Map(entity.Id, f)).ToArray();
}
if (reader.TryGetString(index++, out var productionLocations))
{
- item.ProductionLocations = productionLocations;
+ entity.ProductionLocations = productionLocations;
}
if (reader.TryGetString(index++, out var extraIds))
{
- item.ExtraIds = extraIds;
+ entity.ExtraIds = extraIds;
}
if (reader.TryGetInt32(index++, out var totalBitrate))
{
- item.TotalBitrate = totalBitrate;
+ entity.TotalBitrate = totalBitrate;
}
- if (reader.TryGetString(index++, out var extraTypeString))
+ if (reader.TryGetString(index++, out var extraTypeString) && Enum.TryParse<BaseItemExtraType>(extraTypeString, out var extraType))
{
- item.ExtraType = extraTypeString;
+ entity.ExtraType = extraType;
}
if (reader.TryGetString(index++, out var artists))
{
- item.Artists = artists;
+ entity.Artists = artists;
}
if (reader.TryGetString(index++, out var albumArtists))
{
- item.AlbumArtists = albumArtists;
+ entity.AlbumArtists = albumArtists;
}
if (reader.TryGetString(index++, out var externalId))
{
- item.ExternalId = externalId;
+ entity.ExternalId = externalId;
}
if (reader.TryGetString(index++, out var seriesPresentationUniqueKey))
{
- item.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
+ entity.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
}
if (reader.TryGetString(index++, out var showId))
{
- item.ShowId = showId;
+ entity.ShowId = showId;
}
if (reader.TryGetGuid(index++, out var ownerId))
{
- item.OwnerId = ownerId.ToString("N");
+ entity.OwnerId = ownerId.ToString("N");
}
- return item;
+ return entity;
+ }
+
+ 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;
}
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
index 1cf9e864d..105f5d7af 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
@@ -99,31 +99,6 @@ namespace Jellyfin.Server.Implementations.Tests.Data
return data;
}
- [Theory]
- [MemberData(nameof(ItemImageInfoFromValueString_Valid_TestData))]
- public void ItemImageInfoFromValueString_Valid_Success(string value, ItemImageInfo expected)
- {
- var result = _sqliteItemRepository.ItemImageInfoFromValueString(value)!;
- Assert.Equal(expected.Path, result.Path);
- Assert.Equal(expected.Type, result.Type);
- Assert.Equal(expected.DateModified, result.DateModified);
- Assert.Equal(expected.Width, result.Width);
- Assert.Equal(expected.Height, result.Height);
- Assert.Equal(expected.BlurHash, result.BlurHash);
- }
-
- [Theory]
- [InlineData("")]
- [InlineData("*")]
- [InlineData("https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0")]
- [InlineData("/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*6374520964785129080*WjQbtJtSO8nhNZ%L_Io#R/oaS<o}-;adXAoIn7j[%hW9s:WGw[nN")] // Invalid modified date
- [InlineData("/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*-637452096478512963*WjQbtJtSO8nhNZ%L_Io#R/oaS<o}-;adXAoIn7j[%hW9s:WGw[nN")] // Negative modified date
- [InlineData("/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Invalid*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN")] // Invalid type
- public void ItemImageInfoFromValueString_Invalid_Null(string value)
- {
- Assert.Null(_sqliteItemRepository.ItemImageInfoFromValueString(value));
- }
-
public static TheoryData<string, ItemImageInfo[]> DeserializeImages_Valid_TestData()
{
var data = new TheoryData<string, ItemImageInfo[]>();
@@ -204,47 +179,6 @@ namespace Jellyfin.Server.Implementations.Tests.Data
return data;
}
- [Theory]
- [MemberData(nameof(DeserializeImages_Valid_TestData))]
- public void DeserializeImages_Valid_Success(string value, ItemImageInfo[] expected)
- {
- var result = _sqliteItemRepository.DeserializeImages(value);
- Assert.Equal(expected.Length, result.Length);
- for (int i = 0; i < expected.Length; i++)
- {
- Assert.Equal(expected[i].Path, result[i].Path);
- Assert.Equal(expected[i].Type, result[i].Type);
- Assert.Equal(expected[i].DateModified, result[i].DateModified);
- Assert.Equal(expected[i].Width, result[i].Width);
- Assert.Equal(expected[i].Height, result[i].Height);
- Assert.Equal(expected[i].BlurHash, result[i].BlurHash);
- }
- }
-
- [Theory]
- [MemberData(nameof(DeserializeImages_ValidAndInvalid_TestData))]
- public void DeserializeImages_ValidAndInvalid_Success(string value, ItemImageInfo[] expected)
- {
- var result = _sqliteItemRepository.DeserializeImages(value);
- Assert.Equal(expected.Length, result.Length);
- for (int i = 0; i < expected.Length; i++)
- {
- Assert.Equal(expected[i].Path, result[i].Path);
- Assert.Equal(expected[i].Type, result[i].Type);
- Assert.Equal(expected[i].DateModified, result[i].DateModified);
- Assert.Equal(expected[i].Width, result[i].Width);
- Assert.Equal(expected[i].Height, result[i].Height);
- Assert.Equal(expected[i].BlurHash, result[i].BlurHash);
- }
- }
-
- [Theory]
- [MemberData(nameof(DeserializeImages_Valid_TestData))]
- public void SerializeImages_Valid_Success(string expected, ItemImageInfo[] value)
- {
- Assert.Equal(expected, _sqliteItemRepository.SerializeImages(value));
- }
-
private sealed class ProviderIdsExtensionsTestsObject : IHasProviderIds
{
public Dictionary<string, string> ProviderIds { get; set; } = new Dictionary<string, string>();