mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-07-09 03:04:24 -04:00
Expanded BaseItem aggregate types
This commit is contained in:
parent
f1ae764041
commit
eb601e944c
@ -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 },
|
||||
|
@ -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; }
|
||||
|
18
Jellyfin.Data/Entities/BaseItemExtraType.cs
Normal file
18
Jellyfin.Data/Entities/BaseItemExtraType.cs
Normal file
@ -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
|
||||
}
|
57
Jellyfin.Data/Entities/BaseItemImageInfo.cs
Normal file
57
Jellyfin.Data/Entities/BaseItemImageInfo.cs
Normal file
@ -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; }
|
||||
}
|
26
Jellyfin.Data/Entities/BaseItemMetadataField.cs
Normal file
26
Jellyfin.Data/Entities/BaseItemMetadataField.cs
Normal file
@ -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; }
|
||||
}
|
25
Jellyfin.Data/Entities/BaseItemTrailerType.cs
Normal file
25
Jellyfin.Data/Entities/BaseItemTrailerType.cs
Normal file
@ -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; }
|
||||
}
|
14
Jellyfin.Data/Entities/EnumLikeTable.cs
Normal file
14
Jellyfin.Data/Entities/EnumLikeTable.cs
Normal file
@ -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; }
|
||||
}
|
76
Jellyfin.Data/Entities/ImageInfoImageType.cs
Normal file
76
Jellyfin.Data/Entities/ImageInfoImageType.cs
Normal file
@ -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
|
||||
}
|
37
Jellyfin.Data/Entities/ProgramAudioEntity.cs
Normal file
37
Jellyfin.Data/Entities/ProgramAudioEntity.cs
Normal file
@ -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
|
||||
}
|
@ -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)
|
||||
private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
|
||||
{
|
||||
if (images.Length == 0)
|
||||
return new BaseItemImageInfo()
|
||||
{
|
||||
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();
|
||||
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)
|
||||
private static ItemImageInfo Map(BaseItemImageInfo e)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return new ItemImageInfo()
|
||||
{
|
||||
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];
|
||||
}
|
||||
|
||||
private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
|
||||
{
|
||||
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('|', '\\'));
|
||||
}
|
||||
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>();
|
||||
|
@ -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>();
|
||||
|
1540
Jellyfin.Server.Implementations/Migrations/20241009225800_ExpandedBaseItemFields.Designer.cs
generated
Normal file
1540
Jellyfin.Server.Implementations/Migrations/20241009225800_ExpandedBaseItemFields.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
});
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>();
|
||||
|
Loading…
x
Reference in New Issue
Block a user