diff options
| author | JPVenson <6794763+JPVenson@users.noreply.github.com> | 2024-10-09 15:20:42 +0000 |
|---|---|---|
| committer | JPVenson <6794763+JPVenson@users.noreply.github.com> | 2024-10-09 15:20:42 +0000 |
| commit | 01d834f21abcb65d246b18762b79001929fe845b (patch) | |
| tree | bc9f61cdb7805a9968f46ab2d7aa9e28bfe2200e | |
| parent | c2844bda3b7605257d7b2f8d146077cea6dd0b08 (diff) | |
Fixed (most) tests
13 files changed, 185 insertions, 1109 deletions
diff --git a/Jellyfin.Data/Entities/BaseItemEntity.cs b/Jellyfin.Data/Entities/BaseItemEntity.cs index 5348c8746..dbe5a5372 100644 --- a/Jellyfin.Data/Entities/BaseItemEntity.cs +++ b/Jellyfin.Data/Entities/BaseItemEntity.cs @@ -156,28 +156,12 @@ public class BaseItemEntity public Guid? ParentId { get; set; } - public BaseItemEntity? Parent { get; set; } - - public ICollection<BaseItemEntity>? DirectChildren { get; set; } - public Guid? TopParentId { get; set; } - public BaseItemEntity? TopParent { get; set; } - - public ICollection<BaseItemEntity>? AllChildren { get; set; } - public Guid? SeasonId { get; set; } - public BaseItemEntity? Season { get; set; } - - public ICollection<BaseItemEntity>? SeasonEpisodes { get; set; } - public Guid? SeriesId { get; set; } - public ICollection<BaseItemEntity>? SeriesEpisodes { get; set; } - - public BaseItemEntity? Series { get; set; } - public ICollection<People>? Peoples { get; set; } public ICollection<UserData>? UserData { get; set; } @@ -191,4 +175,14 @@ public class BaseItemEntity public ICollection<BaseItemProvider>? Provider { get; set; } public ICollection<AncestorId>? AncestorIds { 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; } + // public BaseItemEntity? Season { get; set; } + // public BaseItemEntity? Parent { get; set; } + // public ICollection<BaseItemEntity>? DirectChildren { get; set; } + // public BaseItemEntity? TopParent { get; set; } + // public ICollection<BaseItemEntity>? AllChildren { get; set; } + // public ICollection<BaseItemEntity>? SeasonEpisodes { get; set; } } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index d5a1be679..480d83eb1 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -5,20 +5,16 @@ using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Linq.Expressions; -using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; -using System.Threading.Channels; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; -using MediaBrowser.Controller.Playlists; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; @@ -26,6 +22,7 @@ using MediaBrowser.Model.Querying; using Microsoft.EntityFrameworkCore; using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem; using BaseItemEntity = Jellyfin.Data.Entities.BaseItemEntity; +#pragma warning disable RS0030 // Do not use banned APIs namespace Jellyfin.Server.Implementations.Item; @@ -66,12 +63,12 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr using var context = dbProvider.CreateDbContext(); using var transaction = context.Database.BeginTransaction(); - context.Peoples.Where(e => e.ItemId.Equals(id)).ExecuteDelete(); - context.Chapters.Where(e => e.ItemId.Equals(id)).ExecuteDelete(); - context.MediaStreamInfos.Where(e => e.ItemId.Equals(id)).ExecuteDelete(); - context.AncestorIds.Where(e => e.ItemId.Equals(id)).ExecuteDelete(); - context.ItemValues.Where(e => e.ItemId.Equals(id)).ExecuteDelete(); - context.BaseItems.Where(e => e.Id.Equals(id)).ExecuteDelete(); + context.Peoples.Where(e => e.ItemId == id).ExecuteDelete(); + context.Chapters.Where(e => e.ItemId == id).ExecuteDelete(); + context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete(); + 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.SaveChanges(); transaction.Commit(); } @@ -113,8 +110,8 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr PrepareFilterQuery(filter); using var context = dbProvider.CreateDbContext(); - var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter) - .DistinctBy(e => e.Id); + var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter); + // .DistinctBy(e => e.Id); var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter); if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey) @@ -266,8 +263,7 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr PrepareFilterQuery(filter); using var context = dbProvider.CreateDbContext(); - var dbQuery = TranslateQuery(context.BaseItems, context, filter) - .DistinctBy(e => e.Id); + var dbQuery = TranslateQuery(context.BaseItems, context, filter); if (filter.Limit.HasValue || filter.StartIndex.HasValue) { var offset = filter.StartIndex ?? 0; @@ -299,6 +295,7 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr return dbQuery.Count(); } +#pragma warning disable CA1307 // Specify StringComparison for clarity private IQueryable<BaseItemEntity> TranslateQuery( IQueryable<BaseItemEntity> baseQuery, JellyfinDbContext context, @@ -419,7 +416,7 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr if (!string.IsNullOrEmpty(filter.SearchTerm)) { - baseQuery = baseQuery.Where(e => e.CleanName!.Contains(filter.SearchTerm, StringComparison.InvariantCultureIgnoreCase) || (e.OriginalTitle != null && e.OriginalTitle.Contains(filter.SearchTerm, StringComparison.InvariantCultureIgnoreCase))); + baseQuery = baseQuery.Where(e => e.CleanName!.Contains(filter.SearchTerm) || (e.OriginalTitle != null && e.OriginalTitle.Contains(filter.SearchTerm))); } if (filter.IsFolder.HasValue) @@ -474,18 +471,15 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr baseQuery = baseQuery.Where(e => includeTypeName.Contains(e.Type)); } - if (filter.ChannelIds.Count == 1) + if (filter.ChannelIds.Count > 0) { - baseQuery = baseQuery.Where(e => e.ChannelId == filter.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture)); - } - else if (filter.ChannelIds.Count > 1) - { - baseQuery = baseQuery.Where(e => filter.ChannelIds.Select(f => f.ToString("N", CultureInfo.InvariantCulture)).Contains(e.ChannelId)); + var channelIds = filter.ChannelIds.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).ToArray(); + baseQuery = baseQuery.Where(e => channelIds.Contains(e.ChannelId)); } if (!filter.ParentId.IsEmpty()) { - baseQuery = baseQuery.Where(e => e.ParentId.Equals(filter.ParentId)); + baseQuery = baseQuery.Where(e => e.ParentId!.Value == filter.ParentId); } if (!string.IsNullOrWhiteSpace(filter.Path)) @@ -591,7 +585,8 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr if (filter.TrailerTypes.Length > 0) { - baseQuery = baseQuery.Where(e => filter.TrailerTypes.Any(f => e.TrailerTypes!.Contains(f.ToString(), StringComparison.OrdinalIgnoreCase))); + var trailerTypes = filter.TrailerTypes.Select(e => e.ToString()).ToArray(); + baseQuery = baseQuery.Where(e => trailerTypes.Any(f => e.TrailerTypes!.Contains(f))); } if (filter.IsAiring.HasValue) @@ -611,7 +606,7 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr baseQuery = baseQuery .Where(e => context.Peoples.Where(w => context.BaseItems.Where(w => filter.PersonIds.Contains(w.Id)).Any(f => f.Name == w.Name)) - .Any(f => f.ItemId.Equals(e.Id))); + .Any(f => f.ItemId == e.Id)); } if (!string.IsNullOrWhiteSpace(filter.Person)) @@ -649,12 +644,12 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr { baseQuery = baseQuery.Where(e => e.CleanName == filter.NameContains - || e.OriginalTitle!.Contains(filter.NameContains!, StringComparison.Ordinal)); + || e.OriginalTitle!.Contains(filter.NameContains!)); } if (!string.IsNullOrWhiteSpace(filter.NameStartsWith)) { - baseQuery = baseQuery.Where(e => e.SortName!.Contains(filter.NameStartsWith, StringComparison.OrdinalIgnoreCase)); + baseQuery = baseQuery.Where(e => e.SortName!.Contains(filter.NameStartsWith)); } if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater)) @@ -671,31 +666,32 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr if (filter.ImageTypes.Length > 0) { - baseQuery = baseQuery.Where(e => filter.ImageTypes.Any(f => e.Images!.Contains(f.ToString(), StringComparison.InvariantCulture))); + var imgTypes = filter.ImageTypes.Select(e => e.ToString()).ToArray(); + baseQuery = baseQuery.Where(e => imgTypes.Any(f => e.Images!.Contains(f))); } if (filter.IsLiked.HasValue) { baseQuery = baseQuery - .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(filter.User!.Id) && f.Key == e.UserDataKey)!.Rating >= UserItemData.MinLikeValue); + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id && f.Key == e.UserDataKey)!.Rating >= UserItemData.MinLikeValue); } if (filter.IsFavoriteOrLiked.HasValue) { baseQuery = baseQuery - .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(filter.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite == filter.IsFavoriteOrLiked); + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id && f.Key == e.UserDataKey)!.IsFavorite == filter.IsFavoriteOrLiked); } if (filter.IsFavorite.HasValue) { baseQuery = baseQuery - .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(filter.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite == filter.IsFavorite); + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id && f.Key == e.UserDataKey)!.IsFavorite == filter.IsFavorite); } if (filter.IsPlayed.HasValue) { baseQuery = baseQuery - .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(filter.User!.Id) && f.Key == e.UserDataKey)!.Played == filter.IsPlayed.Value); + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id && f.Key == e.UserDataKey)!.Played == filter.IsPlayed.Value); } if (filter.IsResumable.HasValue) @@ -703,12 +699,12 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr if (filter.IsResumable.Value) { baseQuery = baseQuery - .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(filter.User!.Id) && f.Key == e.UserDataKey)!.PlaybackPositionTicks > 0); + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id && f.Key == e.UserDataKey)!.PlaybackPositionTicks > 0); } else { baseQuery = baseQuery - .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(filter.User!.Id) && f.Key == e.UserDataKey)!.PlaybackPositionTicks == 0); + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id && f.Key == e.UserDataKey)!.PlaybackPositionTicks == 0); } } @@ -925,7 +921,7 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value) { baseQuery = baseQuery - .Where(e => e.ParentId.HasValue && context.BaseItems.Any(f => f.Id.Equals(e.ParentId.Value))); + .Where(e => e.ParentId.HasValue && context.BaseItems.Any(f => f.Id == e.ParentId.Value)); } if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value) @@ -1048,11 +1044,11 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr var enableItemsByName = (filter.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0; if (enableItemsByName && includedItemByNameTypes.Count > 0) { - baseQuery = baseQuery.Where(e => includedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => w.Equals(e.TopParentId!.Value))); + baseQuery = baseQuery.Where(e => includedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => w == e.TopParentId!.Value)); } else { - baseQuery = baseQuery.Where(e => queryTopParentIds.Any(w => w.Equals(e.TopParentId!.Value))); + baseQuery = baseQuery.Where(e => queryTopParentIds.Any(w => w == e.TopParentId!.Value)); } } @@ -1064,7 +1060,7 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey)) { baseQuery = baseQuery - .Where(e => context.BaseItems.Where(f => f.PresentationUniqueKey == filter.AncestorWithPresentationUniqueKey).Any(f => f.AncestorIds!.Any(w => w.ItemId.Equals(f.Id)))); + .Where(e => context.BaseItems.Where(f => f.PresentationUniqueKey == filter.AncestorWithPresentationUniqueKey).Any(f => f.AncestorIds!.Any(w => w.ItemId == f.Id))); } if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey)) @@ -1090,7 +1086,7 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr .Where(e => e.ItemValues!.Where(e => e.Type == 6) .Any(f => filter.IncludeInheritedTags.Contains(f.CleanValue)) || - (e.ParentId.HasValue && context.ItemValues.Where(w => w.ItemId.Equals(e.ParentId.Value))!.Where(e => e.Type == 6) + (e.ParentId.HasValue && context.ItemValues.Where(w => w.ItemId == e.ParentId.Value)!.Where(e => e.Type == 6) .Any(f => filter.IncludeInheritedTags.Contains(f.CleanValue)))); } @@ -1112,21 +1108,23 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr if (filter.SeriesStatuses.Length > 0) { + var seriesStatus = filter.SeriesStatuses.Select(e => e.ToString()).ToArray(); baseQuery = baseQuery - .Where(e => filter.SeriesStatuses.Any(f => e.Data!.Contains(f.ToString(), StringComparison.InvariantCultureIgnoreCase))); + .Where(e => seriesStatus.Any(f => e.Data!.Contains(f))); } if (filter.BoxSetLibraryFolders.Length > 0) { + var boxsetFolders = filter.BoxSetLibraryFolders.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).ToArray(); baseQuery = baseQuery - .Where(e => filter.BoxSetLibraryFolders.Any(f => e.Data!.Contains(f.ToString("N", CultureInfo.InvariantCulture), StringComparison.InvariantCultureIgnoreCase))); + .Where(e => boxsetFolders.Any(f => e.Data!.Contains(f))); } if (filter.VideoTypes.Length > 0) { var videoTypeBs = filter.VideoTypes.Select(e => $"\"VideoType\":\"" + e + "\""); baseQuery = baseQuery - .Where(e => videoTypeBs.Any(f => e.Data!.Contains(f, StringComparison.InvariantCultureIgnoreCase))); + .Where(e => videoTypeBs.Any(f => e.Data!.Contains(f))); } if (filter.Is3D.HasValue) @@ -1134,12 +1132,12 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr if (filter.Is3D.Value) { baseQuery = baseQuery - .Where(e => e.Data!.Contains("Video3DFormat", StringComparison.InvariantCultureIgnoreCase)); + .Where(e => e.Data!.Contains("Video3DFormat")); } else { baseQuery = baseQuery - .Where(e => !e.Data!.Contains("Video3DFormat", StringComparison.InvariantCultureIgnoreCase)); + .Where(e => !e.Data!.Contains("Video3DFormat")); } } @@ -1148,12 +1146,12 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr if (filter.IsPlaceHolder.Value) { baseQuery = baseQuery - .Where(e => e.Data!.Contains("IsPlaceHolder\":true", StringComparison.InvariantCultureIgnoreCase)); + .Where(e => e.Data!.Contains("IsPlaceHolder\":true")); } else { baseQuery = baseQuery - .Where(e => !e.Data!.Contains("IsPlaceHolder\":true", StringComparison.InvariantCultureIgnoreCase)); + .Where(e => !e.Data!.Contains("IsPlaceHolder\":true")); } } @@ -1212,7 +1210,7 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr using var db = dbProvider.CreateDbContext(); db.BaseItems - .Where(e => e.Id.Equals(item.Id)) + .Where(e => e.Id == item.Id) .ExecuteUpdate(e => e.SetProperty(f => f.Images, images)); } @@ -1246,11 +1244,20 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr } using var context = dbProvider.CreateDbContext(); + using var transaction = context.Database.BeginTransaction(); foreach (var item in tuples) { var entity = Map(item.Item); - context.BaseItems.Add(entity); + if (!context.BaseItems.Any(e => e.Id == entity.Id)) + { + context.BaseItems.Add(entity); + } + else + { + context.BaseItems.Attach(entity).State = EntityState.Modified; + } + context.AncestorIds.Where(e => e.ItemId == entity.Id).ExecuteDelete(); if (item.Item.SupportsAncestors && item.AncestorIds != null) { foreach (var ancestorId in item.AncestorIds) @@ -1260,13 +1267,13 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr Item = entity, AncestorIdText = ancestorId.ToString(), Id = ancestorId, - ItemId = entity.Id + ItemId = Guid.Empty }); } } var itemValues = GetItemValuesToSave(item.Item, item.InheritedTags); - context.ItemValues.Where(e => e.ItemId.Equals(entity.Id)).ExecuteDelete(); + context.ItemValues.Where(e => e.ItemId == entity.Id).ExecuteDelete(); foreach (var itemValue in itemValues) { context.ItemValues.Add(new() @@ -1275,12 +1282,13 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr Type = itemValue.MagicNumber, Value = itemValue.Value, CleanValue = GetCleanValue(itemValue.Value), - ItemId = entity.Id + ItemId = Guid.Empty }); } } - context.SaveChanges(true); + context.SaveChanges(); + transaction.Commit(); } /// <inheritdoc cref="IItemRepository" /> @@ -1292,7 +1300,7 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr } using var context = dbProvider.CreateDbContext(); - var item = context.BaseItems.FirstOrDefault(e => e.Id.Equals(id)); + var item = context.BaseItems.FirstOrDefault(e => e.Id == id); if (item is null) { return null; @@ -1380,7 +1388,7 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr dto.Audio = Enum.Parse<ProgramAudio>(entity.Audio); } - dto.ExtraIds = entity.ExtraIds?.Split('|').Select(e => Guid.Parse(e)).ToArray(); + dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? null : entity.ExtraIds.Split('|').Select(e => Guid.Parse(e)).ToArray(); dto.ProductionLocations = entity.ProductionLocations?.Split('|'); dto.Studios = entity.Studios?.Split('|'); dto.Tags = entity.Tags?.Split('|'); @@ -1535,8 +1543,8 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr entity.Audio = dto.Audio?.ToString(); entity.ExtraType = dto.ExtraType?.ToString(); - entity.ExtraIds = string.Join('|', dto.ExtraIds); - entity.ProductionLocations = string.Join('|', dto.ProductionLocations); + 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; @@ -1628,15 +1636,15 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr .Where(e => itemValueTypes.Contains(e.Type)); if (withItemTypes.Count > 0) { - query = query.Where(e => context.BaseItems.Where(e => withItemTypes.Contains(e.Type)).Any(f => f.ItemValues!.Any(w => w.ItemId.Equals(e.ItemId)))); + query = query.Where(e => context.BaseItems.Where(e => withItemTypes.Contains(e.Type)).Any(f => f.ItemValues!.Any(w => w.ItemId == e.ItemId))); } if (excludeItemTypes.Count > 0) { - query = query.Where(e => !context.BaseItems.Where(e => withItemTypes.Contains(e.Type)).Any(f => f.ItemValues!.Any(w => w.ItemId.Equals(e.ItemId)))); + query = query.Where(e => !context.BaseItems.Where(e => withItemTypes.Contains(e.Type)).Any(f => f.ItemValues!.Any(w => w.ItemId == e.ItemId))); } - query = query.DistinctBy(e => e.CleanValue); + // query = query.DistinctBy(e => e.CleanValue); return query.Select(e => e.CleanValue).ToImmutableArray(); } @@ -2131,12 +2139,12 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr ItemSortBy.AirTime => e => e.SortName, // TODO ItemSortBy.Runtime => e => e.RunTimeTicks, ItemSortBy.Random => e => EF.Functions.Random(), - ItemSortBy.DatePlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.LastPlayedDate, - ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.PlayCount, - ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite, + ItemSortBy.DatePlayed => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id && f.Key == e.UserDataKey)!.LastPlayedDate, + ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id && f.Key == e.UserDataKey)!.PlayCount, + ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id && f.Key == e.UserDataKey)!.IsFavorite, ItemSortBy.IsFolder => e => e.IsFolder, - ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played, - ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played, + ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id && f.Key == e.UserDataKey)!.Played, + ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id && f.Key == e.UserDataKey)!.Played, ItemSortBy.DateLastContentAdded => e => e.DateLastMediaAdded, ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.Type == 0).Select(f => f.CleanValue), ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.Type == 1).Select(f => f.CleanValue), diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs index c1d6d58cd..a9eda1b64 100644 --- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs +++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs @@ -4,21 +4,19 @@ using Jellyfin.Data.Entities; using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Interfaces; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Implementations; /// <inheritdoc/> -public class JellyfinDbContext : DbContext +/// <summary> +/// Initializes a new instance of the <see cref="JellyfinDbContext"/> class. +/// </summary> +/// <param name="options">The database context options.</param> +/// <param name="logger">Logger.</param> +public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILogger<JellyfinDbContext> logger) : DbContext(options) { /// <summary> - /// Initializes a new instance of the <see cref="JellyfinDbContext"/> class. - /// </summary> - /// <param name="options">The database context options.</param> - public JellyfinDbContext(DbContextOptions<JellyfinDbContext> options) : base(options) - { - } - - /// <summary> /// Gets the <see cref="DbSet{TEntity}"/> containing the access schedules. /// </summary> public DbSet<AccessSchedule> AccessSchedules => Set<AccessSchedule>(); @@ -228,7 +226,15 @@ public class JellyfinDbContext : DbContext saveEntity.OnSavingChanges(); } - return base.SaveChanges(); + try + { + return base.SaveChanges(); + } + catch (Exception e) + { + logger.LogError(e, "Error trying to save changes."); + throw; + } } /// <inheritdoc /> diff --git a/Jellyfin.Server.Implementations/Migrations/20240907123425_UserDataInJfLib.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20240907123425_UserDataInJfLib.Designer.cs deleted file mode 100644 index 609faa1e6..000000000 --- a/Jellyfin.Server.Implementations/Migrations/20240907123425_UserDataInJfLib.Designer.cs +++ /dev/null @@ -1,775 +0,0 @@ -// <auto-generated /> -using System; -using Jellyfin.Server.Implementations; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Jellyfin.Server.Implementations.Migrations -{ - [DbContext(typeof(JellyfinDbContext))] - [Migration("20240907123425_UserDataInJfLib")] - partial class UserDataInJfLib - { - /// <inheritdoc /> - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); - - modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => - { - b.Property<int>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property<int>("DayOfWeek") - .HasColumnType("INTEGER"); - - b.Property<double>("EndHour") - .HasColumnType("REAL"); - - b.Property<double>("StartHour") - .HasColumnType("REAL"); - - b.Property<Guid>("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AccessSchedules"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => - { - b.Property<int>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property<DateTime>("DateCreated") - .HasColumnType("TEXT"); - - b.Property<string>("ItemId") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property<int>("LogSeverity") - .HasColumnType("INTEGER"); - - b.Property<string>("Name") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property<string>("Overview") - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property<uint>("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property<string>("ShortOverview") - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property<string>("Type") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property<Guid>("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("DateCreated"); - - b.ToTable("ActivityLogs"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => - { - b.Property<int>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property<string>("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property<Guid>("ItemId") - .HasColumnType("TEXT"); - - b.Property<string>("Key") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property<Guid>("UserId") - .HasColumnType("TEXT"); - - b.Property<string>("Value") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "ItemId", "Client", "Key") - .IsUnique(); - - b.ToTable("CustomItemDisplayPreferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => - { - b.Property<int>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property<int>("ChromecastVersion") - .HasColumnType("INTEGER"); - - b.Property<string>("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property<string>("DashboardTheme") - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property<bool>("EnableNextVideoInfoOverlay") - .HasColumnType("INTEGER"); - - b.Property<int?>("IndexBy") - .HasColumnType("INTEGER"); - - b.Property<Guid>("ItemId") - .HasColumnType("TEXT"); - - b.Property<int>("ScrollDirection") - .HasColumnType("INTEGER"); - - b.Property<bool>("ShowBackdrop") - .HasColumnType("INTEGER"); - - b.Property<bool>("ShowSidebar") - .HasColumnType("INTEGER"); - - b.Property<int>("SkipBackwardLength") - .HasColumnType("INTEGER"); - - b.Property<int>("SkipForwardLength") - .HasColumnType("INTEGER"); - - b.Property<string>("TvHome") - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property<Guid>("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "ItemId", "Client") - .IsUnique(); - - b.ToTable("DisplayPreferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => - { - b.Property<int>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property<int>("DisplayPreferencesId") - .HasColumnType("INTEGER"); - - b.Property<int>("Order") - .HasColumnType("INTEGER"); - - b.Property<int>("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("DisplayPreferencesId"); - - b.ToTable("HomeSection"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => - { - b.Property<int>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property<DateTime>("LastModified") - .HasColumnType("TEXT"); - - b.Property<string>("Path") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property<Guid?>("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("ImageInfos"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => - { - b.Property<int>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property<string>("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property<int?>("IndexBy") - .HasColumnType("INTEGER"); - - b.Property<Guid>("ItemId") - .HasColumnType("TEXT"); - - b.Property<bool>("RememberIndexing") - .HasColumnType("INTEGER"); - - b.Property<bool>("RememberSorting") - .HasColumnType("INTEGER"); - - b.Property<string>("SortBy") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("TEXT"); - - b.Property<int>("SortOrder") - .HasColumnType("INTEGER"); - - b.Property<Guid>("UserId") - .HasColumnType("TEXT"); - - b.Property<int>("ViewType") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("ItemDisplayPreferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => - { - b.Property<Guid>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property<long>("EndTicks") - .HasColumnType("INTEGER"); - - b.Property<Guid>("ItemId") - .HasColumnType("TEXT"); - - b.Property<string>("SegmentProviderId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property<long>("StartTicks") - .HasColumnType("INTEGER"); - - b.Property<int>("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("MediaSegments"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => - { - b.Property<int>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property<int>("Kind") - .HasColumnType("INTEGER"); - - b.Property<Guid?>("Permission_Permissions_Guid") - .HasColumnType("TEXT"); - - b.Property<uint>("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property<Guid?>("UserId") - .HasColumnType("TEXT"); - - b.Property<bool>("Value") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "Kind") - .IsUnique() - .HasFilter("[UserId] IS NOT NULL"); - - b.ToTable("Permissions"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => - { - b.Property<int>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property<int>("Kind") - .HasColumnType("INTEGER"); - - b.Property<Guid?>("Preference_Preferences_Guid") - .HasColumnType("TEXT"); - - b.Property<uint>("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property<Guid?>("UserId") - .HasColumnType("TEXT"); - - b.Property<string>("Value") - .IsRequired() - .HasMaxLength(65535) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "Kind") - .IsUnique() - .HasFilter("[UserId] IS NOT NULL"); - - b.ToTable("Preferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => - { - b.Property<int>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property<string>("AccessToken") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property<DateTime>("DateCreated") - .HasColumnType("TEXT"); - - b.Property<DateTime>("DateLastActivity") - .HasColumnType("TEXT"); - - b.Property<string>("Name") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AccessToken") - .IsUnique(); - - b.ToTable("ApiKeys"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => - { - b.Property<int>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property<string>("AccessToken") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property<string>("AppName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("TEXT"); - - b.Property<string>("AppVersion") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property<DateTime>("DateCreated") - .HasColumnType("TEXT"); - - b.Property<DateTime>("DateLastActivity") - .HasColumnType("TEXT"); - - b.Property<DateTime>("DateModified") - .HasColumnType("TEXT"); - - b.Property<string>("DeviceId") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property<string>("DeviceName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("TEXT"); - - b.Property<bool>("IsActive") - .HasColumnType("INTEGER"); - - b.Property<Guid>("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("DeviceId"); - - b.HasIndex("AccessToken", "DateLastActivity"); - - b.HasIndex("DeviceId", "DateLastActivity"); - - b.HasIndex("UserId", "DeviceId"); - - b.ToTable("Devices"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => - { - b.Property<int>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property<string>("CustomName") - .HasColumnType("TEXT"); - - b.Property<string>("DeviceId") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("DeviceId") - .IsUnique(); - - b.ToTable("DeviceOptions"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => - { - b.Property<Guid>("ItemId") - .HasColumnType("TEXT"); - - b.Property<int>("Width") - .HasColumnType("INTEGER"); - - b.Property<int>("Bandwidth") - .HasColumnType("INTEGER"); - - b.Property<int>("Height") - .HasColumnType("INTEGER"); - - b.Property<int>("Interval") - .HasColumnType("INTEGER"); - - b.Property<int>("ThumbnailCount") - .HasColumnType("INTEGER"); - - b.Property<int>("TileHeight") - .HasColumnType("INTEGER"); - - b.Property<int>("TileWidth") - .HasColumnType("INTEGER"); - - b.HasKey("ItemId", "Width"); - - b.ToTable("TrickplayInfos"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.User", b => - { - b.Property<Guid>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property<string>("AudioLanguagePreference") - .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.Property<string>("AuthenticationProviderId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.Property<string>("CastReceiverId") - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property<bool>("DisplayCollectionsView") - .HasColumnType("INTEGER"); - - b.Property<bool>("DisplayMissingEpisodes") - .HasColumnType("INTEGER"); - - b.Property<bool>("EnableAutoLogin") - .HasColumnType("INTEGER"); - - b.Property<bool>("EnableLocalPassword") - .HasColumnType("INTEGER"); - - b.Property<bool>("EnableNextEpisodeAutoPlay") - .HasColumnType("INTEGER"); - - b.Property<bool>("EnableUserPreferenceAccess") - .HasColumnType("INTEGER"); - - b.Property<bool>("HidePlayedInLatest") - .HasColumnType("INTEGER"); - - b.Property<long>("InternalId") - .HasColumnType("INTEGER"); - - b.Property<int>("InvalidLoginAttemptCount") - .HasColumnType("INTEGER"); - - b.Property<DateTime?>("LastActivityDate") - .HasColumnType("TEXT"); - - b.Property<DateTime?>("LastLoginDate") - .HasColumnType("TEXT"); - - b.Property<int?>("LoginAttemptsBeforeLockout") - .HasColumnType("INTEGER"); - - b.Property<int>("MaxActiveSessions") - .HasColumnType("INTEGER"); - - b.Property<int?>("MaxParentalAgeRating") - .HasColumnType("INTEGER"); - - b.Property<bool>("MustUpdatePassword") - .HasColumnType("INTEGER"); - - b.Property<string>("Password") - .HasMaxLength(65535) - .HasColumnType("TEXT"); - - b.Property<string>("PasswordResetProviderId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.Property<bool>("PlayDefaultAudioTrack") - .HasColumnType("INTEGER"); - - b.Property<bool>("RememberAudioSelections") - .HasColumnType("INTEGER"); - - b.Property<bool>("RememberSubtitleSelections") - .HasColumnType("INTEGER"); - - b.Property<int?>("RemoteClientBitrateLimit") - .HasColumnType("INTEGER"); - - b.Property<uint>("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property<string>("SubtitleLanguagePreference") - .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.Property<int>("SubtitleMode") - .HasColumnType("INTEGER"); - - b.Property<int>("SyncPlayAccess") - .HasColumnType("INTEGER"); - - b.Property<string>("Username") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("TEXT") - .UseCollation("NOCASE"); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => - { - b.Property<int?>("AudioStreamIndex") - .HasColumnType("INTEGER"); - - b.Property<bool>("IsFavorite") - .HasColumnType("INTEGER"); - - b.Property<string>("Key") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property<DateTime?>("LastPlayedDate") - .HasColumnType("TEXT"); - - b.Property<bool?>("Likes") - .HasColumnType("INTEGER"); - - b.Property<int>("PlayCount") - .HasColumnType("INTEGER"); - - b.Property<long>("PlaybackPositionTicks") - .HasColumnType("INTEGER"); - - b.Property<bool>("Played") - .HasColumnType("INTEGER"); - - b.Property<double?>("Rating") - .HasColumnType("REAL"); - - b.Property<int?>("SubtitleStreamIndex") - .HasColumnType("INTEGER"); - - b.Property<Guid>("UserId") - .HasColumnType("TEXT"); - - b.HasIndex("UserId"); - - b.HasIndex("Key", "UserId") - .IsUnique(); - - b.HasIndex("Key", "UserId", "IsFavorite"); - - b.HasIndex("Key", "UserId", "LastPlayedDate"); - - b.HasIndex("Key", "UserId", "PlaybackPositionTicks"); - - b.HasIndex("Key", "UserId", "Played"); - - b.ToTable("UserData"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("AccessSchedules") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("DisplayPreferences") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => - { - b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) - .WithMany("HomeSections") - .HasForeignKey("DisplayPreferencesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithOne("ProfileImage") - .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("ItemDisplayPreferences") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("Permissions") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("Preferences") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => - { - b.HasOne("Jellyfin.Data.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => - { - b.HasOne("Jellyfin.Data.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => - { - b.Navigation("HomeSections"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.User", b => - { - b.Navigation("AccessSchedules"); - - b.Navigation("DisplayPreferences"); - - b.Navigation("ItemDisplayPreferences"); - - b.Navigation("Permissions"); - - b.Navigation("Preferences"); - - b.Navigation("ProfileImage"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Jellyfin.Server.Implementations/Migrations/20240907123425_UserDataInJfLib.cs b/Jellyfin.Server.Implementations/Migrations/20240907123425_UserDataInJfLib.cs deleted file mode 100644 index cb9a01f5b..000000000 --- a/Jellyfin.Server.Implementations/Migrations/20240907123425_UserDataInJfLib.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Jellyfin.Server.Implementations.Migrations -{ - /// <inheritdoc /> - public partial class UserDataInJfLib : Migration - { - /// <inheritdoc /> - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "UserData", - columns: table => new - { - Key = table.Column<string>(type: "TEXT", nullable: false), - Rating = table.Column<double>(type: "REAL", nullable: true), - PlaybackPositionTicks = table.Column<long>(type: "INTEGER", nullable: false), - PlayCount = table.Column<int>(type: "INTEGER", nullable: false), - IsFavorite = table.Column<bool>(type: "INTEGER", nullable: false), - LastPlayedDate = table.Column<DateTime>(type: "TEXT", nullable: true), - Played = table.Column<bool>(type: "INTEGER", nullable: false), - AudioStreamIndex = table.Column<int>(type: "INTEGER", nullable: true), - SubtitleStreamIndex = table.Column<int>(type: "INTEGER", nullable: true), - Likes = table.Column<bool>(type: "INTEGER", nullable: true), - UserId = table.Column<Guid>(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.ForeignKey( - name: "FK_UserData_Users_UserId", - column: x => x.UserId, - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_UserData_Key_UserId", - table: "UserData", - columns: new[] { "Key", "UserId" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_UserData_Key_UserId_IsFavorite", - table: "UserData", - columns: new[] { "Key", "UserId", "IsFavorite" }); - - migrationBuilder.CreateIndex( - name: "IX_UserData_Key_UserId_LastPlayedDate", - table: "UserData", - columns: new[] { "Key", "UserId", "LastPlayedDate" }); - - migrationBuilder.CreateIndex( - name: "IX_UserData_Key_UserId_PlaybackPositionTicks", - table: "UserData", - columns: new[] { "Key", "UserId", "PlaybackPositionTicks" }); - - migrationBuilder.CreateIndex( - name: "IX_UserData_Key_UserId_Played", - table: "UserData", - columns: new[] { "Key", "UserId", "Played" }); - - migrationBuilder.CreateIndex( - name: "IX_UserData_UserId", - table: "UserData", - column: "UserId"); - } - - /// <inheritdoc /> - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "UserData"); - } - } -} diff --git a/Jellyfin.Server.Implementations/Migrations/20241009112234_BaseItemRefactor.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20241009132112_BaseItemRefactor.Designer.cs index b3e028298..8e8e6c125 100644 --- a/Jellyfin.Server.Implementations/Migrations/20241009112234_BaseItemRefactor.Designer.cs +++ b/Jellyfin.Server.Implementations/Migrations/20241009132112_BaseItemRefactor.Designer.cs @@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Jellyfin.Server.Implementations.Migrations { [DbContext(typeof(JellyfinDbContext))] - [Migration("20241009112234_BaseItemRefactor")] + [Migration("20241009132112_BaseItemRefactor")] partial class BaseItemRefactor { /// <inheritdoc /> @@ -379,10 +379,6 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("PresentationUniqueKey"); - b.HasIndex("SeasonId"); - - b.HasIndex("SeriesId"); - b.HasIndex("TopParentId", "Id"); b.HasIndex("UserDataKey", "Type"); @@ -1275,33 +1271,6 @@ namespace Jellyfin.Server.Implementations.Migrations b.Navigation("Item"); }); - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Parent") - .WithMany("DirectChildren") - .HasForeignKey("ParentId"); - - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Season") - .WithMany("SeasonEpisodes") - .HasForeignKey("SeasonId"); - - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Series") - .WithMany("SeriesEpisodes") - .HasForeignKey("SeriesId"); - - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "TopParent") - .WithMany("AllChildren") - .HasForeignKey("TopParentId"); - - b.Navigation("Parent"); - - b.Navigation("Season"); - - b.Navigation("Series"); - - b.Navigation("TopParent"); - }); - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => { b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") @@ -1436,14 +1405,10 @@ namespace Jellyfin.Server.Implementations.Migrations modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => { - b.Navigation("AllChildren"); - b.Navigation("AncestorIds"); b.Navigation("Chapters"); - b.Navigation("DirectChildren"); - b.Navigation("ItemValues"); b.Navigation("MediaStreams"); @@ -1452,10 +1417,6 @@ namespace Jellyfin.Server.Implementations.Migrations b.Navigation("Provider"); - b.Navigation("SeasonEpisodes"); - - b.Navigation("SeriesEpisodes"); - b.Navigation("UserData"); }); diff --git a/Jellyfin.Server.Implementations/Migrations/20241009112234_BaseItemRefactor.cs b/Jellyfin.Server.Implementations/Migrations/20241009132112_BaseItemRefactor.cs index f51e385e0..caa731c15 100644 --- a/Jellyfin.Server.Implementations/Migrations/20241009112234_BaseItemRefactor.cs +++ b/Jellyfin.Server.Implementations/Migrations/20241009132112_BaseItemRefactor.cs @@ -11,21 +11,6 @@ namespace Jellyfin.Server.Implementations.Migrations /// <inheritdoc /> protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.DropIndex( - name: "IX_UserData_Key_UserId", - table: "UserData"); - - migrationBuilder.AddColumn<Guid>( - name: "BaseItemEntityId", - table: "UserData", - type: "TEXT", - nullable: true); - - migrationBuilder.AddPrimaryKey( - name: "PK_UserData", - table: "UserData", - columns: new[] { "Key", "UserId" }); - migrationBuilder.CreateTable( name: "BaseItems", columns: table => new @@ -109,26 +94,6 @@ namespace Jellyfin.Server.Implementations.Migrations constraints: table => { table.PrimaryKey("PK_BaseItems", x => x.Id); - table.ForeignKey( - name: "FK_BaseItems_BaseItems_ParentId", - column: x => x.ParentId, - principalTable: "BaseItems", - principalColumn: "Id"); - table.ForeignKey( - name: "FK_BaseItems_BaseItems_SeasonId", - column: x => x.SeasonId, - principalTable: "BaseItems", - principalColumn: "Id"); - table.ForeignKey( - name: "FK_BaseItems_BaseItems_SeriesId", - column: x => x.SeriesId, - principalTable: "BaseItems", - principalColumn: "Id"); - table.ForeignKey( - name: "FK_BaseItems_BaseItems_TopParentId", - column: x => x.TopParentId, - principalTable: "BaseItems", - principalColumn: "Id"); }); migrationBuilder.CreateTable( @@ -318,10 +283,38 @@ namespace Jellyfin.Server.Implementations.Migrations onDelete: ReferentialAction.Cascade); }); - migrationBuilder.CreateIndex( - name: "IX_UserData_BaseItemEntityId", - table: "UserData", - column: "BaseItemEntityId"); + migrationBuilder.CreateTable( + name: "UserData", + columns: table => new + { + Key = table.Column<string>(type: "TEXT", nullable: false), + UserId = table.Column<Guid>(type: "TEXT", nullable: false), + Rating = table.Column<double>(type: "REAL", nullable: true), + PlaybackPositionTicks = table.Column<long>(type: "INTEGER", nullable: false), + PlayCount = table.Column<int>(type: "INTEGER", nullable: false), + IsFavorite = table.Column<bool>(type: "INTEGER", nullable: false), + LastPlayedDate = table.Column<DateTime>(type: "TEXT", nullable: true), + Played = table.Column<bool>(type: "INTEGER", nullable: false), + AudioStreamIndex = table.Column<int>(type: "INTEGER", nullable: true), + SubtitleStreamIndex = table.Column<int>(type: "INTEGER", nullable: true), + Likes = table.Column<bool>(type: "INTEGER", nullable: true), + BaseItemEntityId = table.Column<Guid>(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_UserData", x => new { x.Key, x.UserId }); + table.ForeignKey( + name: "FK_UserData_BaseItems_BaseItemEntityId", + column: x => x.BaseItemEntityId, + principalTable: "BaseItems", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_UserData_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); migrationBuilder.CreateIndex( name: "IX_AncestorIds_Id", @@ -369,16 +362,6 @@ namespace Jellyfin.Server.Implementations.Migrations column: "PresentationUniqueKey"); migrationBuilder.CreateIndex( - name: "IX_BaseItems_SeasonId", - table: "BaseItems", - column: "SeasonId"); - - migrationBuilder.CreateIndex( - name: "IX_BaseItems_SeriesId", - table: "BaseItems", - column: "SeriesId"); - - migrationBuilder.CreateIndex( name: "IX_BaseItems_TopParentId_Id", table: "BaseItems", columns: new[] { "TopParentId", "Id" }); @@ -453,21 +436,40 @@ namespace Jellyfin.Server.Implementations.Migrations table: "Peoples", column: "Name"); - migrationBuilder.AddForeignKey( - name: "FK_UserData_BaseItems_BaseItemEntityId", + migrationBuilder.CreateIndex( + name: "IX_UserData_BaseItemEntityId", + table: "UserData", + column: "BaseItemEntityId"); + + migrationBuilder.CreateIndex( + name: "IX_UserData_Key_UserId_IsFavorite", + table: "UserData", + columns: new[] { "Key", "UserId", "IsFavorite" }); + + migrationBuilder.CreateIndex( + name: "IX_UserData_Key_UserId_LastPlayedDate", + table: "UserData", + columns: new[] { "Key", "UserId", "LastPlayedDate" }); + + migrationBuilder.CreateIndex( + name: "IX_UserData_Key_UserId_PlaybackPositionTicks", table: "UserData", - column: "BaseItemEntityId", - principalTable: "BaseItems", - principalColumn: "Id"); + columns: new[] { "Key", "UserId", "PlaybackPositionTicks" }); + + migrationBuilder.CreateIndex( + name: "IX_UserData_Key_UserId_Played", + table: "UserData", + columns: new[] { "Key", "UserId", "Played" }); + + migrationBuilder.CreateIndex( + name: "IX_UserData_UserId", + table: "UserData", + column: "UserId"); } /// <inheritdoc /> protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropForeignKey( - name: "FK_UserData_BaseItems_BaseItemEntityId", - table: "UserData"); - migrationBuilder.DropTable( name: "AncestorIds"); @@ -490,25 +492,10 @@ namespace Jellyfin.Server.Implementations.Migrations name: "Peoples"); migrationBuilder.DropTable( - name: "BaseItems"); + name: "UserData"); - migrationBuilder.DropPrimaryKey( - name: "PK_UserData", - table: "UserData"); - - migrationBuilder.DropIndex( - name: "IX_UserData_BaseItemEntityId", - table: "UserData"); - - migrationBuilder.DropColumn( - name: "BaseItemEntityId", - table: "UserData"); - - migrationBuilder.CreateIndex( - name: "IX_UserData_Key_UserId", - table: "UserData", - columns: new[] { "Key", "UserId" }, - unique: true); + migrationBuilder.DropTable( + name: "BaseItems"); } } } diff --git a/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs index 940cf7c5d..500c4a1c7 100644 --- a/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs +++ b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Logging.Abstractions; namespace Jellyfin.Server.Implementations.Migrations { @@ -14,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations var optionsBuilder = new DbContextOptionsBuilder<JellyfinDbContext>(); optionsBuilder.UseSqlite("Data Source=jellyfin.db"); - return new JellyfinDbContext(optionsBuilder.Options); + return new JellyfinDbContext(optionsBuilder.Options, NullLogger<JellyfinDbContext>.Instance); } } } diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index f74f7d791..dd280489b 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -376,10 +376,6 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("PresentationUniqueKey"); - b.HasIndex("SeasonId"); - - b.HasIndex("SeriesId"); - b.HasIndex("TopParentId", "Id"); b.HasIndex("UserDataKey", "Type"); @@ -1272,33 +1268,6 @@ namespace Jellyfin.Server.Implementations.Migrations b.Navigation("Item"); }); - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Parent") - .WithMany("DirectChildren") - .HasForeignKey("ParentId"); - - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Season") - .WithMany("SeasonEpisodes") - .HasForeignKey("SeasonId"); - - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Series") - .WithMany("SeriesEpisodes") - .HasForeignKey("SeriesId"); - - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "TopParent") - .WithMany("AllChildren") - .HasForeignKey("TopParentId"); - - b.Navigation("Parent"); - - b.Navigation("Season"); - - b.Navigation("Series"); - - b.Navigation("TopParent"); - }); - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => { b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") @@ -1433,14 +1402,10 @@ namespace Jellyfin.Server.Implementations.Migrations modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => { - b.Navigation("AllChildren"); - b.Navigation("AncestorIds"); b.Navigation("Chapters"); - b.Navigation("DirectChildren"); - b.Navigation("ItemValues"); b.Navigation("MediaStreams"); @@ -1449,10 +1414,6 @@ namespace Jellyfin.Server.Implementations.Migrations b.Navigation("Provider"); - b.Navigation("SeasonEpisodes"); - - b.Navigation("SeriesEpisodes"); - b.Navigation("UserData"); }); diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs index d74b94784..6c36a1591 100644 --- a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs +++ b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs @@ -15,10 +15,11 @@ public class BaseItemConfiguration : IEntityTypeConfiguration<BaseItemEntity> public void Configure(EntityTypeBuilder<BaseItemEntity> builder) { builder.HasKey(e => e.Id); - builder.HasOne(e => e.Parent).WithMany(e => e.DirectChildren).HasForeignKey(e => e.ParentId); - builder.HasOne(e => e.TopParent).WithMany(e => e.AllChildren).HasForeignKey(e => e.TopParentId); - builder.HasOne(e => e.Season).WithMany(e => e.SeasonEpisodes).HasForeignKey(e => e.SeasonId); - builder.HasOne(e => e.Series).WithMany(e => e.SeriesEpisodes).HasForeignKey(e => e.SeriesId); + // TODO: See rant in entity file. + // builder.HasOne(e => e.Parent).WithMany(e => e.DirectChildren).HasForeignKey(e => e.ParentId); + // builder.HasOne(e => e.TopParent).WithMany(e => e.AllChildren).HasForeignKey(e => e.TopParentId); + // builder.HasOne(e => e.Season).WithMany(e => e.SeasonEpisodes).HasForeignKey(e => e.SeasonId); + // builder.HasOne(e => e.Series).WithMany(e => e.SeriesEpisodes).HasForeignKey(e => e.SeriesId); builder.HasMany(e => e.Peoples); builder.HasMany(e => e.UserData); builder.HasMany(e => e.ItemValues); diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs index eabbe73cd..ef69885fc 100644 --- a/MediaBrowser.Controller/Providers/MetadataResult.cs +++ b/MediaBrowser.Controller/Providers/MetadataResult.cs @@ -36,7 +36,7 @@ namespace MediaBrowser.Controller.Providers public IReadOnlyList<PersonInfo> People { get => _people; - set => _people = value.ToList(); + set => _people = value?.ToList(); } public bool HasMetadata { get; set; } diff --git a/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs b/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs index cedcaf9c0..b32ecf6ec 100644 --- a/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs +++ b/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs @@ -330,7 +330,7 @@ namespace Jellyfin.Providers.Tests.Manager MetadataService<Movie, MovieInfo>.MergeBaseItemData(source, target, lockedFields, replaceData, false); actualValue = target.People; - return newValue?.Equals(actualValue) ?? actualValue is null; + return newValue?.SequenceEqual((IEnumerable<PersonInfo>)actualValue!) ?? actualValue is null; } /// <summary> diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs index bf3bfdad4..02a77516f 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs @@ -13,7 +13,7 @@ using Xunit.Priority; namespace Jellyfin.Server.Integration.Tests.Controllers; -[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)] +// [TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)] public sealed class LibraryStructureControllerTests : IClassFixture<JellyfinApplicationFactory> { private readonly JellyfinApplicationFactory _factory; @@ -62,12 +62,23 @@ public sealed class LibraryStructureControllerTests : IClassFixture<JellyfinAppl } [Fact] - [Priority(0)] + [Priority(-2)] public async Task UpdateLibraryOptions_Valid_Success() { var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); + var createBody = new AddVirtualFolderDto() + { + LibraryOptions = new LibraryOptions() + { + Enabled = false + } + }; + + using var createResponse = await client.PostAsJsonAsync("Library/VirtualFolders?name=test&refreshLibrary=true", createBody, _jsonOptions); + Assert.Equal(HttpStatusCode.NoContent, createResponse.StatusCode); + using var response = await client.GetAsync("Library/VirtualFolders"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -80,13 +91,13 @@ public sealed class LibraryStructureControllerTests : IClassFixture<JellyfinAppl Assert.False(options.Enabled); options.Enabled = true; - var body = new UpdateLibraryOptionsDto() + var existBody = new UpdateLibraryOptionsDto() { Id = Guid.Parse(library.ItemId), LibraryOptions = options }; - using var response2 = await client.PostAsJsonAsync("Library/VirtualFolders/LibraryOptions", body, _jsonOptions); + using var response2 = await client.PostAsJsonAsync("Library/VirtualFolders/LibraryOptions", existBody, _jsonOptions); Assert.Equal(HttpStatusCode.NoContent, response2.StatusCode); } |
