Expanded BaseItem aggregate types

This commit is contained in:
JPVenson 2024-10-09 23:01:54 +00:00
parent f1ae764041
commit eb601e944c
19 changed files with 2485 additions and 426 deletions

View File

@ -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 },

View File

@ -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; }

View 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
}

View 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; }
}

View 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; }
}

View 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; }
}

View 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; }
}

View 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
}

View 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
}

View File

@ -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>();

View File

@ -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>();

File diff suppressed because it is too large Load Diff

View File

@ -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);
}
}
}

View File

@ -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");
});

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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>();