From 9ec85a0f180dd3ea1582ad60f3fb8d55861f2fdf Mon Sep 17 00:00:00 2001 From: elfalem Date: Sun, 22 Sep 2024 20:50:29 -0400 Subject: Allow Playlists access for users with allowed tags configured (#12686) --- MediaBrowser.Controller/Entities/BaseItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 414488853f..eb605f6c87 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1608,7 +1608,7 @@ namespace MediaBrowser.Controller.Entities } var parent = GetParents().FirstOrDefault() ?? this; - if (parent is UserRootFolder or AggregateFolder) + if (parent is UserRootFolder or AggregateFolder or UserView) { return true; } -- cgit v1.2.3 From be48cdd9e90ed147c5526ef3fed0624bcbad7741 Mon Sep 17 00:00:00 2001 From: JPVenson <6794763+JPVenson@users.noreply.github.com> Date: Wed, 9 Oct 2024 09:53:39 +0000 Subject: Naming refactoring and WIP porting of new interface repositories --- Emby.Server.Implementations/ApplicationHost.cs | 13 +- Emby.Server.Implementations/Data/ItemTypeLookup.cs | 139 ++ .../MediaEncoder/EncodingManager.cs | 4 +- Jellyfin.Data/Entities/AncestorId.cs | 2 +- Jellyfin.Data/Entities/AttachmentStreamInfo.cs | 2 +- Jellyfin.Data/Entities/BaseItem.cs | 176 -- Jellyfin.Data/Entities/BaseItemEntity.cs | 177 ++ Jellyfin.Data/Entities/BaseItemProvider.cs | 23 +- Jellyfin.Data/Entities/Chapter.cs | 2 +- Jellyfin.Data/Entities/ItemValue.cs | 23 +- Jellyfin.Data/Entities/MediaStreamInfo.cs | 2 +- Jellyfin.Data/Entities/People.cs | 34 +- .../Item/BaseItemManager.cs | 2333 -------------------- .../Item/BaseItemRepository.cs | 2233 +++++++++++++++++++ .../Item/ChapterManager.cs | 99 - .../Item/ChapterRepository.cs | 123 ++ .../Item/MediaAttachmentManager.cs | 73 - .../Item/MediaAttachmentRepository.cs | 73 + .../Item/MediaStreamManager.cs | 201 -- .../Item/MediaStreamRepository.cs | 201 ++ .../Item/PeopleManager.cs | 164 -- .../Item/PeopleRepository.cs | 165 ++ .../JellyfinDbContext.cs | 2 +- .../ModelConfiguration/BaseItemConfiguration.cs | 4 +- .../BaseItemProviderConfiguration.cs | 2 +- MediaBrowser.Controller/Chapters/ChapterManager.cs | 24 - .../Chapters/IChapterManager.cs | 35 - .../Chapters/IChapterRepository.cs | 49 + MediaBrowser.Controller/Drawing/IImageProcessor.cs | 25 + MediaBrowser.Controller/Entities/BaseItem.cs | 7 +- .../Persistence/IItemRepository.cs | 1 - .../Persistence/IItemTypeLookup.cs | 57 + .../Persistence/IMediaAttachmentManager.cs | 29 - .../Persistence/IMediaAttachmentRepository.cs | 28 + .../Persistence/IMediaStreamManager.cs | 28 - .../Persistence/IMediaStreamRepository.cs | 31 + .../Persistence/IPeopleManager.cs | 34 - .../Persistence/IPeopleRepository.cs | 33 + .../MediaInfo/FFProbeVideoInfo.cs | 4 +- MediaBrowser.Providers/MediaInfo/ProbeProvider.cs | 4 +- src/Jellyfin.Drawing/ImageProcessor.cs | 25 + 41 files changed, 3459 insertions(+), 3225 deletions(-) create mode 100644 Emby.Server.Implementations/Data/ItemTypeLookup.cs delete mode 100644 Jellyfin.Data/Entities/BaseItem.cs create mode 100644 Jellyfin.Data/Entities/BaseItemEntity.cs delete mode 100644 Jellyfin.Server.Implementations/Item/BaseItemManager.cs create mode 100644 Jellyfin.Server.Implementations/Item/BaseItemRepository.cs delete mode 100644 Jellyfin.Server.Implementations/Item/ChapterManager.cs create mode 100644 Jellyfin.Server.Implementations/Item/ChapterRepository.cs delete mode 100644 Jellyfin.Server.Implementations/Item/MediaAttachmentManager.cs create mode 100644 Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs delete mode 100644 Jellyfin.Server.Implementations/Item/MediaStreamManager.cs create mode 100644 Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs delete mode 100644 Jellyfin.Server.Implementations/Item/PeopleManager.cs create mode 100644 Jellyfin.Server.Implementations/Item/PeopleRepository.cs delete mode 100644 MediaBrowser.Controller/Chapters/ChapterManager.cs delete mode 100644 MediaBrowser.Controller/Chapters/IChapterManager.cs create mode 100644 MediaBrowser.Controller/Chapters/IChapterRepository.cs create mode 100644 MediaBrowser.Controller/Persistence/IItemTypeLookup.cs delete mode 100644 MediaBrowser.Controller/Persistence/IMediaAttachmentManager.cs create mode 100644 MediaBrowser.Controller/Persistence/IMediaAttachmentRepository.cs delete mode 100644 MediaBrowser.Controller/Persistence/IMediaStreamManager.cs create mode 100644 MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs delete mode 100644 MediaBrowser.Controller/Persistence/IPeopleManager.cs create mode 100644 MediaBrowser.Controller/Persistence/IPeopleRepository.cs (limited to 'MediaBrowser.Controller/Entities') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index bdf013b5d6..fbec4726fc 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -40,6 +40,7 @@ using Jellyfin.MediaEncoding.Hls.Playlist; using Jellyfin.Networking.Manager; using Jellyfin.Networking.Udp; using Jellyfin.Server.Implementations; +using Jellyfin.Server.Implementations.Item; using Jellyfin.Server.Implementations.MediaSegments; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; @@ -83,7 +84,6 @@ using MediaBrowser.Model.Net; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.System; using MediaBrowser.Model.Tasks; -using MediaBrowser.Providers.Chapters; using MediaBrowser.Providers.Lyric; using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.Tmdb; @@ -494,7 +494,12 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -539,8 +544,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -578,8 +581,6 @@ namespace Emby.Server.Implementations } } - ((SqliteItemRepository)Resolve()).Initialize(); - var localizationManager = (LocalizationManager)Resolve(); await localizationManager.LoadAll().ConfigureAwait(false); diff --git a/Emby.Server.Implementations/Data/ItemTypeLookup.cs b/Emby.Server.Implementations/Data/ItemTypeLookup.cs new file mode 100644 index 0000000000..14dc68a327 --- /dev/null +++ b/Emby.Server.Implementations/Data/ItemTypeLookup.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Threading.Channels; +using Emby.Server.Implementations.Playlists; +using Jellyfin.Data.Enums; +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.Querying; + +namespace Jellyfin.Server.Implementations.Item; + +/// +/// Provides static topic based lookups for the BaseItemKind. +/// +public class ItemTypeLookup : IItemTypeLookup +{ + /// + /// Gets all values of the ItemFields type. + /// + public IReadOnlyList AllItemFields { get; } = Enum.GetValues(); + + /// + /// Gets all BaseItemKinds that are considered Programs. + /// + public IReadOnlyList ProgramTypes { get; } = + [ + BaseItemKind.Program, + BaseItemKind.TvChannel, + BaseItemKind.LiveTvProgram, + BaseItemKind.LiveTvChannel + ]; + + /// + /// Gets all BaseItemKinds that should be excluded from parent lookup. + /// + public IReadOnlyList ProgramExcludeParentTypes { get; } = + [ + BaseItemKind.Series, + BaseItemKind.Season, + BaseItemKind.MusicAlbum, + BaseItemKind.MusicArtist, + BaseItemKind.PhotoAlbum + ]; + + /// + /// Gets all BaseItemKinds that are considered to be provided by services. + /// + public IReadOnlyList ServiceTypes { get; } = + [ + BaseItemKind.TvChannel, + BaseItemKind.LiveTvChannel + ]; + + /// + /// Gets all BaseItemKinds that have a StartDate. + /// + public IReadOnlyList StartDateTypes { get; } = + [ + BaseItemKind.Program, + BaseItemKind.LiveTvProgram + ]; + + /// + /// Gets all BaseItemKinds that are considered Series. + /// + public IReadOnlyList SeriesTypes { get; } = + [ + BaseItemKind.Book, + BaseItemKind.AudioBook, + BaseItemKind.Episode, + BaseItemKind.Season + ]; + + /// + /// Gets all BaseItemKinds that are not to be evaluated for Artists. + /// + public IReadOnlyList ArtistExcludeParentTypes { get; } = + [ + BaseItemKind.Series, + BaseItemKind.Season, + BaseItemKind.PhotoAlbum + ]; + + /// + /// Gets all BaseItemKinds that are considered Artists. + /// + public IReadOnlyList ArtistsTypes { get; } = + [ + BaseItemKind.Audio, + BaseItemKind.MusicAlbum, + BaseItemKind.MusicVideo, + BaseItemKind.AudioBook + ]; + + /// + /// Gets mapping for all BaseItemKinds and their expected serialisaition target. + /// + public IDictionary BaseItemKindNames { get; } = new Dictionary() + { + { BaseItemKind.AggregateFolder, typeof(AggregateFolder).FullName }, + { BaseItemKind.Audio, typeof(Audio).FullName }, + { BaseItemKind.AudioBook, typeof(AudioBook).FullName }, + { BaseItemKind.BasePluginFolder, typeof(BasePluginFolder).FullName }, + { BaseItemKind.Book, typeof(Book).FullName }, + { BaseItemKind.BoxSet, typeof(BoxSet).FullName }, + { BaseItemKind.Channel, typeof(Channel).FullName }, + { BaseItemKind.CollectionFolder, typeof(CollectionFolder).FullName }, + { BaseItemKind.Episode, typeof(Episode).FullName }, + { BaseItemKind.Folder, typeof(Folder).FullName }, + { BaseItemKind.Genre, typeof(Genre).FullName }, + { BaseItemKind.Movie, typeof(Movie).FullName }, + { BaseItemKind.LiveTvChannel, typeof(LiveTvChannel).FullName }, + { BaseItemKind.LiveTvProgram, typeof(LiveTvProgram).FullName }, + { BaseItemKind.MusicAlbum, typeof(MusicAlbum).FullName }, + { BaseItemKind.MusicArtist, typeof(MusicArtist).FullName }, + { BaseItemKind.MusicGenre, typeof(MusicGenre).FullName }, + { BaseItemKind.MusicVideo, typeof(MusicVideo).FullName }, + { BaseItemKind.Person, typeof(Person).FullName }, + { BaseItemKind.Photo, typeof(Photo).FullName }, + { BaseItemKind.PhotoAlbum, typeof(PhotoAlbum).FullName }, + { BaseItemKind.Playlist, typeof(Playlist).FullName }, + { BaseItemKind.PlaylistsFolder, typeof(PlaylistsFolder).FullName }, + { BaseItemKind.Season, typeof(Season).FullName }, + { BaseItemKind.Series, typeof(Series).FullName }, + { BaseItemKind.Studio, typeof(Studio).FullName }, + { BaseItemKind.Trailer, typeof(Trailer).FullName }, + { BaseItemKind.TvChannel, typeof(LiveTvChannel).FullName }, + { BaseItemKind.TvProgram, typeof(LiveTvProgram).FullName }, + { BaseItemKind.UserRootFolder, typeof(UserRootFolder).FullName }, + { BaseItemKind.UserView, typeof(UserView).FullName }, + { BaseItemKind.Video, typeof(Video).FullName }, + { BaseItemKind.Year, typeof(Year).FullName } + }.AsReadOnly(); +} diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index eb55e32c50..ea78968617 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.MediaEncoder private readonly IFileSystem _fileSystem; private readonly ILogger _logger; private readonly IMediaEncoder _encoder; - private readonly IChapterManager _chapterManager; + private readonly IChapterRepository _chapterManager; private readonly ILibraryManager _libraryManager; /// @@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.MediaEncoder ILogger logger, IFileSystem fileSystem, IMediaEncoder encoder, - IChapterManager chapterManager, + IChapterRepository chapterManager, ILibraryManager libraryManager) { _logger = logger; diff --git a/Jellyfin.Data/Entities/AncestorId.cs b/Jellyfin.Data/Entities/AncestorId.cs index dc83b763ee..3839b1ae46 100644 --- a/Jellyfin.Data/Entities/AncestorId.cs +++ b/Jellyfin.Data/Entities/AncestorId.cs @@ -13,7 +13,7 @@ public class AncestorId public Guid ItemId { get; set; } - public required BaseItem Item { get; set; } + public required BaseItemEntity Item { get; set; } public string? AncestorIdText { get; set; } } diff --git a/Jellyfin.Data/Entities/AttachmentStreamInfo.cs b/Jellyfin.Data/Entities/AttachmentStreamInfo.cs index 858465424b..056d5b05ec 100644 --- a/Jellyfin.Data/Entities/AttachmentStreamInfo.cs +++ b/Jellyfin.Data/Entities/AttachmentStreamInfo.cs @@ -7,7 +7,7 @@ public class AttachmentStreamInfo { public required Guid ItemId { get; set; } - public required BaseItem Item { get; set; } + public required BaseItemEntity Item { get; set; } public required int Index { get; set; } diff --git a/Jellyfin.Data/Entities/BaseItem.cs b/Jellyfin.Data/Entities/BaseItem.cs deleted file mode 100644 index 0e67a7ca45..0000000000 --- a/Jellyfin.Data/Entities/BaseItem.cs +++ /dev/null @@ -1,176 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities; - -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member -public class BaseItem -{ - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - - public Guid Id { get; set; } - - public required string Type { get; set; } - - public string? Data { get; set; } - - public Guid? ParentId { get; set; } - - public string? Path { get; set; } - - public DateTime StartDate { get; set; } - - public DateTime EndDate { get; set; } - - public string? ChannelId { get; set; } - - public bool IsMovie { get; set; } - - public float? CommunityRating { get; set; } - - public string? CustomRating { get; set; } - - public int? IndexNumber { get; set; } - - public bool IsLocked { get; set; } - - public string? Name { get; set; } - - public string? OfficialRating { get; set; } - - public string? MediaType { get; set; } - - public string? Overview { get; set; } - - public int? ParentIndexNumber { get; set; } - - public DateTime? PremiereDate { get; set; } - - public int? ProductionYear { get; set; } - - public string? Genres { get; set; } - - public string? SortName { get; set; } - - public string? ForcedSortName { get; set; } - - public long? RunTimeTicks { get; set; } - - public DateTime? DateCreated { get; set; } - - public DateTime? DateModified { get; set; } - - public bool IsSeries { get; set; } - - public string? EpisodeTitle { get; set; } - - public bool IsRepeat { get; set; } - - public string? PreferredMetadataLanguage { get; set; } - - public string? PreferredMetadataCountryCode { get; set; } - - public DateTime? DateLastRefreshed { get; set; } - - public DateTime? DateLastSaved { get; set; } - - 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; } - - public bool IsFolder { get; set; } - - public int? InheritedParentalRatingValue { get; set; } - - public string? UnratedType { get; set; } - - public Guid? TopParentId { get; set; } - - public string? TrailerTypes { get; set; } - - public float? CriticRating { get; set; } - - public string? CleanName { get; set; } - - public string? PresentationUniqueKey { get; set; } - - public string? OriginalTitle { get; set; } - - public string? PrimaryVersionId { get; set; } - - public DateTime? DateLastMediaAdded { get; set; } - - public string? Album { get; set; } - - public float? LUFS { get; set; } - - public float? NormalizationGain { get; set; } - - public bool IsVirtualItem { get; set; } - - public string? SeriesName { get; set; } - - public string? UserDataKey { get; set; } - - public string? SeasonName { get; set; } - - public Guid? SeasonId { get; set; } - - public Guid? SeriesId { get; set; } - - public string? ExternalSeriesId { get; set; } - - 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 string? Artists { get; set; } - - public string? AlbumArtists { get; set; } - - public string? ExternalId { get; set; } - - public string? SeriesPresentationUniqueKey { get; set; } - - public string? ShowId { get; set; } - - public string? OwnerId { get; set; } - - public int? Width { get; set; } - - public int? Height { get; set; } - - public long? Size { get; set; } - - public ICollection? Peoples { get; set; } - - public ICollection? UserData { get; set; } - - public ICollection? ItemValues { get; set; } - - public ICollection? MediaStreams { get; set; } - - public ICollection? Chapters { get; set; } - - public ICollection? Provider { get; set; } - - public ICollection? AncestorIds { get; set; } -} diff --git a/Jellyfin.Data/Entities/BaseItemEntity.cs b/Jellyfin.Data/Entities/BaseItemEntity.cs new file mode 100644 index 0000000000..92b5caf057 --- /dev/null +++ b/Jellyfin.Data/Entities/BaseItemEntity.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +public class BaseItemEntity +{ + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + + public Guid Id { get; set; } + + public required string Type { get; set; } + + public string? Data { get; set; } + + public Guid? ParentId { get; set; } + + public string? Path { get; set; } + + public DateTime StartDate { get; set; } + + public DateTime EndDate { get; set; } + + public string? ChannelId { get; set; } + + public bool IsMovie { get; set; } + + public float? CommunityRating { get; set; } + + public string? CustomRating { get; set; } + + public int? IndexNumber { get; set; } + + public bool IsLocked { get; set; } + + public string? Name { get; set; } + + public string? OfficialRating { get; set; } + + public string? MediaType { get; set; } + + public string? Overview { get; set; } + + public int? ParentIndexNumber { get; set; } + + public DateTime? PremiereDate { get; set; } + + public int? ProductionYear { get; set; } + + public string? Genres { get; set; } + + public string? SortName { get; set; } + + public string? ForcedSortName { get; set; } + + public long? RunTimeTicks { get; set; } + + public DateTime? DateCreated { get; set; } + + public DateTime? DateModified { get; set; } + + public bool IsSeries { get; set; } + + public string? EpisodeTitle { get; set; } + + public bool IsRepeat { get; set; } + + public string? PreferredMetadataLanguage { get; set; } + + public string? PreferredMetadataCountryCode { get; set; } + + public DateTime? DateLastRefreshed { get; set; } + + public DateTime? DateLastSaved { get; set; } + + 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; } + + public bool IsFolder { get; set; } + + public int? InheritedParentalRatingValue { get; set; } + + public string? UnratedType { get; set; } + + public Guid? TopParentId { get; set; } + + public string? TrailerTypes { get; set; } + + public float? CriticRating { get; set; } + + public string? CleanName { get; set; } + + public string? PresentationUniqueKey { get; set; } + + public string? OriginalTitle { get; set; } + + public string? PrimaryVersionId { get; set; } + + public DateTime? DateLastMediaAdded { get; set; } + + public string? Album { get; set; } + + public float? LUFS { get; set; } + + public float? NormalizationGain { get; set; } + + public bool IsVirtualItem { get; set; } + + public string? SeriesName { get; set; } + + public string? UserDataKey { get; set; } + + public string? SeasonName { get; set; } + + public Guid? SeasonId { get; set; } + + public Guid? SeriesId { get; set; } + + public string? ExternalSeriesId { get; set; } + + 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 string? Artists { get; set; } + + public string? AlbumArtists { get; set; } + + public string? ExternalId { get; set; } + + public string? SeriesPresentationUniqueKey { get; set; } + + public string? ShowId { get; set; } + + public string? OwnerId { get; set; } + + public int? Width { get; set; } + + public int? Height { get; set; } + + public long? Size { get; set; } + +#pragma warning disable CA2227 // Collection properties should be read only + public ICollection? Peoples { get; set; } + + public ICollection? UserData { get; set; } + + public ICollection? ItemValues { get; set; } + + public ICollection? MediaStreams { get; set; } + + public ICollection? Chapters { get; set; } + + public ICollection? Provider { get; set; } + + public ICollection? AncestorIds { get; set; } +} diff --git a/Jellyfin.Data/Entities/BaseItemProvider.cs b/Jellyfin.Data/Entities/BaseItemProvider.cs index 6f8e1c39bb..1fc721d6a2 100644 --- a/Jellyfin.Data/Entities/BaseItemProvider.cs +++ b/Jellyfin.Data/Entities/BaseItemProvider.cs @@ -5,11 +5,28 @@ using System.ComponentModel.DataAnnotations.Schema; namespace Jellyfin.Data.Entities; +/// +/// Represents an Key-Value relaten of an BaseItem's provider. +/// public class BaseItemProvider { + /// + /// Gets or Sets the reference ItemId. + /// public Guid ItemId { get; set; } - public required BaseItem Item { get; set; } - public string ProviderId { get; set; } - public string ProviderValue { get; set; } + /// + /// Gets or Sets the reference BaseItem. + /// + public required BaseItemEntity Item { get; set; } + + /// + /// Gets or Sets the ProvidersId. + /// + public required string ProviderId { get; set; } + + /// + /// Gets or Sets the Providers Value. + /// + public required string ProviderValue { get; set; } } diff --git a/Jellyfin.Data/Entities/Chapter.cs b/Jellyfin.Data/Entities/Chapter.cs index ad119d1c6b..be353b5da4 100644 --- a/Jellyfin.Data/Entities/Chapter.cs +++ b/Jellyfin.Data/Entities/Chapter.cs @@ -10,7 +10,7 @@ public class Chapter { public Guid ItemId { get; set; } - public required BaseItem Item { get; set; } + public required BaseItemEntity Item { get; set; } public required int ChapterIndex { get; set; } diff --git a/Jellyfin.Data/Entities/ItemValue.cs b/Jellyfin.Data/Entities/ItemValue.cs index a3c0908bbe..1063aaa8b2 100644 --- a/Jellyfin.Data/Entities/ItemValue.cs +++ b/Jellyfin.Data/Entities/ItemValue.cs @@ -5,12 +5,33 @@ using System.ComponentModel.DataAnnotations.Schema; namespace Jellyfin.Data.Entities; +/// +/// Represents an ItemValue for a BaseItem. +/// public class ItemValue { + /// + /// Gets or Sets the reference ItemId. + /// public Guid ItemId { get; set; } - public required BaseItem Item { get; set; } + /// + /// Gets or Sets the referenced BaseItem. + /// + public required BaseItemEntity Item { get; set; } + + /// + /// Gets or Sets the Type. + /// public required int Type { get; set; } + + /// + /// Gets or Sets the Value. + /// public required string Value { get; set; } + + /// + /// Gets or Sets the sanatised Value. + /// public required string CleanValue { get; set; } } diff --git a/Jellyfin.Data/Entities/MediaStreamInfo.cs b/Jellyfin.Data/Entities/MediaStreamInfo.cs index 3b89ca62f8..992f33ecf8 100644 --- a/Jellyfin.Data/Entities/MediaStreamInfo.cs +++ b/Jellyfin.Data/Entities/MediaStreamInfo.cs @@ -7,7 +7,7 @@ public class MediaStreamInfo { public Guid ItemId { get; set; } - public required BaseItem Item { get; set; } + public required BaseItemEntity Item { get; set; } public int StreamIndex { get; set; } diff --git a/Jellyfin.Data/Entities/People.cs b/Jellyfin.Data/Entities/People.cs index 014a0f1c97..8eb23f5e4d 100644 --- a/Jellyfin.Data/Entities/People.cs +++ b/Jellyfin.Data/Entities/People.cs @@ -4,14 +4,44 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace Jellyfin.Data.Entities; + +/// +/// People entity. +/// public class People { - public Guid ItemId { get; set; } - public BaseItem Item { get; set; } + /// + /// Gets or Sets The ItemId. + /// + public required Guid ItemId { get; set; } + + /// + /// Gets or Sets Reference Item. + /// + public required BaseItemEntity Item { get; set; } + /// + /// Gets or Sets the Persons Name. + /// public required string Name { get; set; } + + /// + /// Gets or Sets the Role. + /// public string? Role { get; set; } + + /// + /// Gets or Sets the Type. + /// public string? PersonType { get; set; } + + /// + /// Gets or Sets the SortOrder. + /// public int? SortOrder { get; set; } + + /// + /// Gets or Sets the ListOrder. + /// public int? ListOrder { get; set; } } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemManager.cs b/Jellyfin.Server.Implementations/Item/BaseItemManager.cs deleted file mode 100644 index 66cc765f35..0000000000 --- a/Jellyfin.Server.Implementations/Item/BaseItemManager.cs +++ /dev/null @@ -1,2333 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -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; -using MediaBrowser.Model.Querying; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Internal; -using Microsoft.EntityFrameworkCore.Query.SqlExpressions; -using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem; -using BaseItemEntity = Jellyfin.Data.Entities.BaseItem; - -namespace Jellyfin.Server.Implementations.Item; - -/// -/// Handles all storage logic for BaseItems. -/// -public sealed class BaseItemManager : IItemRepository, IDisposable -{ - private readonly IDbContextFactory _dbProvider; - private readonly IServerApplicationHost _appHost; - - private readonly ItemFields[] _allItemFields = Enum.GetValues(); - - private static readonly BaseItemKind[] _programTypes = new[] - { - BaseItemKind.Program, - BaseItemKind.TvChannel, - BaseItemKind.LiveTvProgram, - BaseItemKind.LiveTvChannel - }; - - private static readonly BaseItemKind[] _programExcludeParentTypes = new[] - { - BaseItemKind.Series, - BaseItemKind.Season, - BaseItemKind.MusicAlbum, - BaseItemKind.MusicArtist, - BaseItemKind.PhotoAlbum - }; - - private static readonly BaseItemKind[] _serviceTypes = new[] - { - BaseItemKind.TvChannel, - BaseItemKind.LiveTvChannel - }; - - private static readonly BaseItemKind[] _startDateTypes = new[] - { - BaseItemKind.Program, - BaseItemKind.LiveTvProgram - }; - - private static readonly BaseItemKind[] _seriesTypes = new[] - { - BaseItemKind.Book, - BaseItemKind.AudioBook, - BaseItemKind.Episode, - BaseItemKind.Season - }; - - private static readonly BaseItemKind[] _artistExcludeParentTypes = new[] - { - BaseItemKind.Series, - BaseItemKind.Season, - BaseItemKind.PhotoAlbum - }; - - private static readonly BaseItemKind[] _artistsTypes = new[] - { - BaseItemKind.Audio, - BaseItemKind.MusicAlbum, - BaseItemKind.MusicVideo, - BaseItemKind.AudioBook - }; - - private static readonly Dictionary _baseItemKindNames = new() - { - { BaseItemKind.AggregateFolder, typeof(AggregateFolder).FullName }, - { BaseItemKind.Audio, typeof(Audio).FullName }, - { BaseItemKind.AudioBook, typeof(AudioBook).FullName }, - { BaseItemKind.BasePluginFolder, typeof(BasePluginFolder).FullName }, - { BaseItemKind.Book, typeof(Book).FullName }, - { BaseItemKind.BoxSet, typeof(BoxSet).FullName }, - { BaseItemKind.Channel, typeof(Channel).FullName }, - { BaseItemKind.CollectionFolder, typeof(CollectionFolder).FullName }, - { BaseItemKind.Episode, typeof(Episode).FullName }, - { BaseItemKind.Folder, typeof(Folder).FullName }, - { BaseItemKind.Genre, typeof(Genre).FullName }, - { BaseItemKind.Movie, typeof(Movie).FullName }, - { BaseItemKind.LiveTvChannel, typeof(LiveTvChannel).FullName }, - { BaseItemKind.LiveTvProgram, typeof(LiveTvProgram).FullName }, - { BaseItemKind.MusicAlbum, typeof(MusicAlbum).FullName }, - { BaseItemKind.MusicArtist, typeof(MusicArtist).FullName }, - { BaseItemKind.MusicGenre, typeof(MusicGenre).FullName }, - { BaseItemKind.MusicVideo, typeof(MusicVideo).FullName }, - { BaseItemKind.Person, typeof(Person).FullName }, - { BaseItemKind.Photo, typeof(Photo).FullName }, - { BaseItemKind.PhotoAlbum, typeof(PhotoAlbum).FullName }, - { BaseItemKind.Playlist, typeof(Playlist).FullName }, - { BaseItemKind.PlaylistsFolder, typeof(PlaylistsFolder).FullName }, - { BaseItemKind.Season, typeof(Season).FullName }, - { BaseItemKind.Series, typeof(Series).FullName }, - { BaseItemKind.Studio, typeof(Studio).FullName }, - { BaseItemKind.Trailer, typeof(Trailer).FullName }, - { BaseItemKind.TvChannel, typeof(LiveTvChannel).FullName }, - { BaseItemKind.TvProgram, typeof(LiveTvProgram).FullName }, - { BaseItemKind.UserRootFolder, typeof(UserRootFolder).FullName }, - { BaseItemKind.UserView, typeof(UserView).FullName }, - { BaseItemKind.Video, typeof(Video).FullName }, - { BaseItemKind.Year, typeof(Year).FullName } - }; - - /// - /// This holds all the types in the running assemblies - /// so that we can de-serialize properly when we don't have strong types. - /// - private static readonly ConcurrentDictionary _typeMap = new ConcurrentDictionary(); - private bool _disposed; - - /// - /// Initializes a new instance of the class. - /// - /// The db factory. - /// The Application host. - public BaseItemManager(IDbContextFactory dbProvider, IServerApplicationHost appHost) - { - _dbProvider = dbProvider; - _appHost = appHost; - } - - /// - public void Dispose() - { - if (_disposed) - { - return; - } - - _disposed = true; - } - - private QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetItemValues(InternalItemsQuery filter, int[] itemValueTypes, string returnType) - { - ArgumentNullException.ThrowIfNull(filter); - - if (!filter.Limit.HasValue) - { - filter.EnableTotalRecordCount = false; - } - - using var context = _dbProvider.CreateDbContext(); - - var innerQuery = new InternalItemsQuery(filter.User) - { - ExcludeItemTypes = filter.ExcludeItemTypes, - IncludeItemTypes = filter.IncludeItemTypes, - MediaTypes = filter.MediaTypes, - AncestorIds = filter.AncestorIds, - ItemIds = filter.ItemIds, - TopParentIds = filter.TopParentIds, - ParentId = filter.ParentId, - IsAiring = filter.IsAiring, - IsMovie = filter.IsMovie, - IsSports = filter.IsSports, - IsKids = filter.IsKids, - IsNews = filter.IsNews, - IsSeries = filter.IsSeries - }; - var query = TranslateQuery(context.BaseItems, context, innerQuery); - - query = query.Where(e => e.Type == returnType && e.ItemValues!.Any(f => e.CleanName == f.CleanValue && itemValueTypes.Contains(f.Type))); - - var outerQuery = new InternalItemsQuery(filter.User) - { - IsPlayed = filter.IsPlayed, - IsFavorite = filter.IsFavorite, - IsFavoriteOrLiked = filter.IsFavoriteOrLiked, - IsLiked = filter.IsLiked, - IsLocked = filter.IsLocked, - NameLessThan = filter.NameLessThan, - NameStartsWith = filter.NameStartsWith, - NameStartsWithOrGreater = filter.NameStartsWithOrGreater, - Tags = filter.Tags, - OfficialRatings = filter.OfficialRatings, - StudioIds = filter.StudioIds, - GenreIds = filter.GenreIds, - Genres = filter.Genres, - Years = filter.Years, - NameContains = filter.NameContains, - SearchTerm = filter.SearchTerm, - SimilarTo = filter.SimilarTo, - ExcludeItemIds = filter.ExcludeItemIds - }; - query = TranslateQuery(query, context, outerQuery) - .OrderBy(e => e.PresentationUniqueKey); - - if (filter.OrderBy.Count != 0 - || filter.SimilarTo is not null - || !string.IsNullOrEmpty(filter.SearchTerm)) - { - query = ApplyOrder(query, filter); - } - else - { - query = query.OrderBy(e => e.SortName); - } - - if (filter.Limit.HasValue || filter.StartIndex.HasValue) - { - var offset = filter.StartIndex ?? 0; - - if (offset > 0) - { - query = query.Skip(offset); - } - - if (filter.Limit.HasValue) - { - query.Take(filter.Limit.Value); - } - } - - var result = new QueryResult<(BaseItem, ItemCounts)>(); - string countText = string.Empty; - if (filter.EnableTotalRecordCount) - { - result.TotalRecordCount = query.DistinctBy(e => e.PresentationUniqueKey).Count(); - } - - var resultQuery = query.Select(e => new - { - item = e, - itemCount = new ItemCounts() - { - SeriesCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Series), - EpisodeCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Episode), - MovieCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Movie), - AlbumCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.MusicAlbum), - ArtistCount = e.ItemValues!.Count(e => e.Type == 0 || e.Type == 1), - SongCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.MusicAlbum), - TrailerCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Trailer), - } - }); - - result.StartIndex = filter.StartIndex ?? 0; - result.Items = resultQuery.ToImmutableArray().Select(e => - { - return (DeserialiseBaseItem(e.item), e.itemCount); - }).ToImmutableArray(); - - return result; - } - - /// - public void DeleteItem(Guid id) - { - ArgumentNullException.ThrowIfNull(id.IsEmpty() ? null : id); - - 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.SaveChanges(); - transaction.Commit(); - } - - /// - public void UpdateInheritedValues() - { - using var context = _dbProvider.CreateDbContext(); - using var transaction = context.Database.BeginTransaction(); - - context.ItemValues.Where(e => e.Type == 6).ExecuteDelete(); - context.ItemValues.AddRange(context.ItemValues.Where(e => e.Type == 4).Select(e => new Data.Entities.ItemValue() - { - CleanValue = e.CleanValue, - ItemId = e.ItemId, - Type = 6, - Value = e.Value, - Item = null! - })); - - context.ItemValues.AddRange( - context.AncestorIds.Where(e => e.AncestorIdText != null).Join(context.ItemValues.Where(e => e.Value != null && e.Type == 4), e => e.Id, e => e.ItemId, (e, f) => new Data.Entities.ItemValue() - { - CleanValue = f.CleanValue, - ItemId = e.ItemId, - Item = null!, - Type = 6, - Value = f.Value - })); - context.SaveChanges(); - - transaction.Commit(); - } - - /// - public IReadOnlyList GetItemIdsList(InternalItemsQuery filter) - { - ArgumentNullException.ThrowIfNull(filter); - PrepareFilterQuery(filter); - - using var context = _dbProvider.CreateDbContext(); - var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter) - .DistinctBy(e => e.Id); - - var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter); - if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey) - { - dbQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).SelectMany(e => e); - } - - if (enableGroupByPresentationUniqueKey) - { - dbQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).SelectMany(e => e); - } - - if (filter.GroupBySeriesPresentationUniqueKey) - { - dbQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).SelectMany(e => e); - } - - dbQuery = ApplyOrder(dbQuery, filter); - - return Pageinate(dbQuery, filter).Select(e => e.Id).ToImmutableArray(); - } - - /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery filter) - { - return GetItemValues(filter, new[] { 0, 1 }, typeof(MusicArtist).FullName!); - } - - /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery filter) - { - return GetItemValues(filter, new[] { 0 }, typeof(MusicArtist).FullName!); - } - - /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery filter) - { - return GetItemValues(filter, new[] { 1 }, typeof(MusicArtist).FullName!); - } - - /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery filter) - { - return GetItemValues(filter, new[] { 3 }, typeof(Studio).FullName!); - } - - /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery filter) - { - return GetItemValues(filter, new[] { 2 }, typeof(Genre).FullName!); - } - - /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery filter) - { - return GetItemValues(filter, new[] { 2 }, typeof(MusicGenre).FullName!); - } - - /// - public IReadOnlyList GetStudioNames() - { - return GetItemValueNames(new[] { 3 }, Array.Empty(), Array.Empty()); - } - - /// - public IReadOnlyList GetAllArtistNames() - { - return GetItemValueNames(new[] { 0, 1 }, Array.Empty(), Array.Empty()); - } - - /// - public IReadOnlyList GetMusicGenreNames() - { - return GetItemValueNames( - new[] { 2 }, - new string[] - { - typeof(Audio).FullName!, - typeof(MusicVideo).FullName!, - typeof(MusicAlbum).FullName!, - typeof(MusicArtist).FullName! - }, - Array.Empty()); - } - - /// - public IReadOnlyList GetGenreNames() - { - return GetItemValueNames( - new[] { 2 }, - Array.Empty(), - new string[] - { - typeof(Audio).FullName!, - typeof(MusicVideo).FullName!, - typeof(MusicAlbum).FullName!, - typeof(MusicArtist).FullName! - }); - } - - /// - public QueryResult GetItems(InternalItemsQuery filter) - { - ArgumentNullException.ThrowIfNull(filter); - if (!filter.EnableTotalRecordCount || (!filter.Limit.HasValue && (filter.StartIndex ?? 0) == 0)) - { - var returnList = GetItemList(filter); - return new QueryResult( - filter.StartIndex, - returnList.Count, - returnList); - } - - PrepareFilterQuery(filter); - var result = new QueryResult(); - - using var context = _dbProvider.CreateDbContext(); - var dbQuery = TranslateQuery(context.BaseItems, context, filter) - .DistinctBy(e => e.Id); - if (filter.EnableTotalRecordCount) - { - result.TotalRecordCount = dbQuery.Count(); - } - - if (filter.Limit.HasValue || filter.StartIndex.HasValue) - { - var offset = filter.StartIndex ?? 0; - - if (offset > 0) - { - dbQuery = dbQuery.Skip(offset); - } - - if (filter.Limit.HasValue) - { - dbQuery = dbQuery.Take(filter.Limit.Value); - } - } - - result.Items = dbQuery.ToList().Select(DeserialiseBaseItem).ToImmutableArray(); - result.StartIndex = filter.StartIndex ?? 0; - return result; - } - - /// - public IReadOnlyList GetItemList(InternalItemsQuery filter) - { - ArgumentNullException.ThrowIfNull(filter); - PrepareFilterQuery(filter); - - using var context = _dbProvider.CreateDbContext(); - var dbQuery = TranslateQuery(context.BaseItems, context, filter) - .DistinctBy(e => e.Id); - if (filter.Limit.HasValue || filter.StartIndex.HasValue) - { - var offset = filter.StartIndex ?? 0; - - if (offset > 0) - { - dbQuery = dbQuery.Skip(offset); - } - - if (filter.Limit.HasValue) - { - dbQuery = dbQuery.Take(filter.Limit.Value); - } - } - - return dbQuery.ToList().Select(DeserialiseBaseItem).ToImmutableArray(); - } - - /// - public int GetCount(InternalItemsQuery filter) - { - ArgumentNullException.ThrowIfNull(filter); - // Hack for right now since we currently don't support filtering out these duplicates within a query - PrepareFilterQuery(filter); - - using var context = _dbProvider.CreateDbContext(); - var dbQuery = TranslateQuery(context.BaseItems, context, filter); - - return dbQuery.Count(); - } - - private IQueryable TranslateQuery( - IQueryable baseQuery, - JellyfinDbContext context, - InternalItemsQuery filter) - { - var minWidth = filter.MinWidth; - var maxWidth = filter.MaxWidth; - var now = DateTime.UtcNow; - - if (filter.IsHD.HasValue) - { - const int Threshold = 1200; - if (filter.IsHD.Value) - { - minWidth = Threshold; - } - else - { - maxWidth = Threshold - 1; - } - } - - if (filter.Is4K.HasValue) - { - const int Threshold = 3800; - if (filter.Is4K.Value) - { - minWidth = Threshold; - } - else - { - maxWidth = Threshold - 1; - } - } - - if (minWidth.HasValue) - { - baseQuery = baseQuery.Where(e => e.Width >= minWidth); - } - - if (filter.MinHeight.HasValue) - { - baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight); - } - - if (maxWidth.HasValue) - { - baseQuery = baseQuery.Where(e => e.Width >= maxWidth); - } - - if (filter.MaxHeight.HasValue) - { - baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight); - } - - if (filter.IsLocked.HasValue) - { - baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked); - } - - var tags = filter.Tags.ToList(); - var excludeTags = filter.ExcludeTags.ToList(); - - if (filter.IsMovie == true) - { - if (filter.IncludeItemTypes.Length == 0 - || filter.IncludeItemTypes.Contains(BaseItemKind.Movie) - || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer)) - { - baseQuery = baseQuery.Where(e => e.IsMovie); - } - } - else if (filter.IsMovie.HasValue) - { - baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie); - } - - if (filter.IsSeries.HasValue) - { - baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries); - } - - if (filter.IsSports.HasValue) - { - if (filter.IsSports.Value) - { - tags.Add("Sports"); - } - else - { - excludeTags.Add("Sports"); - } - } - - if (filter.IsNews.HasValue) - { - if (filter.IsNews.Value) - { - tags.Add("News"); - } - else - { - excludeTags.Add("News"); - } - } - - if (filter.IsKids.HasValue) - { - if (filter.IsKids.Value) - { - tags.Add("Kids"); - } - else - { - excludeTags.Add("Kids"); - } - } - - 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))); - } - - if (filter.IsFolder.HasValue) - { - baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder); - } - - var includeTypes = filter.IncludeItemTypes; - // Only specify excluded types if no included types are specified - if (filter.IncludeItemTypes.Length == 0) - { - var excludeTypes = filter.ExcludeItemTypes; - if (excludeTypes.Length == 1) - { - if (_baseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName)) - { - baseQuery = baseQuery.Where(e => e.Type != excludeTypeName); - } - } - else if (excludeTypes.Length > 1) - { - var excludeTypeName = new List(); - foreach (var excludeType in excludeTypes) - { - if (_baseItemKindNames.TryGetValue(excludeType, out var baseItemKindName)) - { - excludeTypeName.Add(baseItemKindName!); - } - } - - baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type)); - } - } - else if (includeTypes.Length == 1) - { - if (_baseItemKindNames.TryGetValue(includeTypes[0], out var includeTypeName)) - { - baseQuery = baseQuery.Where(e => e.Type == includeTypeName); - } - } - else if (includeTypes.Length > 1) - { - var includeTypeName = new List(); - foreach (var includeType in includeTypes) - { - if (_baseItemKindNames.TryGetValue(includeType, out var baseItemKindName)) - { - includeTypeName.Add(baseItemKindName!); - } - } - - baseQuery = baseQuery.Where(e => includeTypeName.Contains(e.Type)); - } - - if (filter.ChannelIds.Count == 1) - { - 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)); - } - - if (!filter.ParentId.IsEmpty()) - { - baseQuery = baseQuery.Where(e => e.ParentId.Equals(filter.ParentId)); - } - - if (!string.IsNullOrWhiteSpace(filter.Path)) - { - baseQuery = baseQuery.Where(e => e.Path == filter.Path); - } - - if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey)) - { - baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey); - } - - if (filter.MinCommunityRating.HasValue) - { - baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating); - } - - if (filter.MinIndexNumber.HasValue) - { - baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber); - } - - if (filter.MinParentAndIndexNumber.HasValue) - { - baseQuery = baseQuery - .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNumber >= filter.MinParentAndIndexNumber.Value.IndexNumber) || e.ParentIndexNumber > filter.MinParentAndIndexNumber.Value.ParentIndexNumber); - } - - if (filter.MinDateCreated.HasValue) - { - baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated); - } - - if (filter.MinDateLastSaved.HasValue) - { - baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value); - } - - if (filter.MinDateLastSavedForUser.HasValue) - { - baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUser.Value); - } - - if (filter.IndexNumber.HasValue) - { - baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value); - } - - if (filter.ParentIndexNumber.HasValue) - { - baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value); - } - - if (filter.ParentIndexNumberNotEquals.HasValue) - { - baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentIndexNumber == null); - } - - var minEndDate = filter.MinEndDate; - var maxEndDate = filter.MaxEndDate; - - if (filter.HasAired.HasValue) - { - if (filter.HasAired.Value) - { - maxEndDate = DateTime.UtcNow; - } - else - { - minEndDate = DateTime.UtcNow; - } - } - - if (minEndDate.HasValue) - { - baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate); - } - - if (maxEndDate.HasValue) - { - baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate); - } - - if (filter.MinStartDate.HasValue) - { - baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value); - } - - if (filter.MaxStartDate.HasValue) - { - baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value); - } - - if (filter.MinPremiereDate.HasValue) - { - baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MinPremiereDate.Value); - } - - if (filter.MaxPremiereDate.HasValue) - { - baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value); - } - - if (filter.TrailerTypes.Length > 0) - { - baseQuery = baseQuery.Where(e => filter.TrailerTypes.Any(f => e.TrailerTypes!.Contains(f.ToString(), StringComparison.OrdinalIgnoreCase))); - } - - if (filter.IsAiring.HasValue) - { - if (filter.IsAiring.Value) - { - baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now); - } - else - { - baseQuery = baseQuery.Where(e => e.StartDate > now && e.EndDate < now); - } - } - - if (filter.PersonIds.Length > 0) - { - 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))); - } - - if (!string.IsNullOrWhiteSpace(filter.Person)) - { - baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.Name == filter.Person)); - } - - if (!string.IsNullOrWhiteSpace(filter.MinSortName)) - { - // this does not makes sense. - // baseQuery = baseQuery.Where(e => e.SortName >= query.MinSortName); - // whereClauses.Add("SortName>=@MinSortName"); - // statement?.TryBind("@MinSortName", query.MinSortName); - } - - if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId)) - { - baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId); - } - - if (!string.IsNullOrWhiteSpace(filter.ExternalId)) - { - baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId); - } - - if (!string.IsNullOrWhiteSpace(filter.Name)) - { - var cleanName = GetCleanValue(filter.Name); - baseQuery = baseQuery.Where(e => e.CleanName == cleanName); - } - - // These are the same, for now - var nameContains = filter.NameContains; - if (!string.IsNullOrWhiteSpace(nameContains)) - { - baseQuery = baseQuery.Where(e => - e.CleanName == filter.NameContains - || e.OriginalTitle!.Contains(filter.NameContains!, StringComparison.Ordinal)); - } - - if (!string.IsNullOrWhiteSpace(filter.NameStartsWith)) - { - baseQuery = baseQuery.Where(e => e.SortName!.Contains(filter.NameStartsWith, StringComparison.OrdinalIgnoreCase)); - } - - if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater)) - { - // i hate this - baseQuery = baseQuery.Where(e => e.SortName![0] > filter.NameStartsWithOrGreater[0]); - } - - if (!string.IsNullOrWhiteSpace(filter.NameLessThan)) - { - // i hate this - baseQuery = baseQuery.Where(e => e.SortName![0] < filter.NameLessThan[0]); - } - - if (filter.ImageTypes.Length > 0) - { - baseQuery = baseQuery.Where(e => filter.ImageTypes.Any(f => e.Images!.Contains(f.ToString(), StringComparison.InvariantCulture))); - } - - 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); - } - - 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); - } - - 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); - } - - 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); - } - - if (filter.IsResumable.HasValue) - { - if (filter.IsResumable.Value) - { - baseQuery = baseQuery - .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(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); - } - } - - var artistQuery = context.BaseItems.Where(w => filter.ArtistIds.Contains(w.Id)); - - if (filter.ArtistIds.Length > 0) - { - baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.Type <= 1 && artistQuery.Any(w => w.CleanName == f.CleanValue))); - } - - if (filter.AlbumArtistIds.Length > 0) - { - baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.Type == 1 && artistQuery.Any(w => w.CleanName == f.CleanValue))); - } - - if (filter.ContributingArtistIds.Length > 0) - { - var contributingArtists = context.BaseItems.Where(e => filter.ContributingArtistIds.Contains(e.Id)); - baseQuery = baseQuery.Where(e => e.ItemValues!.Any(f => f.Type == 0 && contributingArtists.Any(w => w.CleanName == f.CleanValue))); - } - - if (filter.AlbumIds.Length > 0) - { - baseQuery = baseQuery.Where(e => context.BaseItems.Where(e => filter.AlbumIds.Contains(e.Id)).Any(f => f.Name == e.Album)); - } - - if (filter.ExcludeArtistIds.Length > 0) - { - var excludeArtistQuery = context.BaseItems.Where(w => filter.ExcludeArtistIds.Contains(w.Id)); - baseQuery = baseQuery - .Where(e => !e.ItemValues!.Any(f => f.Type <= 1 && artistQuery.Any(w => w.CleanName == f.CleanValue))); - } - - if (filter.GenreIds.Count > 0) - { - baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.Type == 2 && context.BaseItems.Where(w => filter.GenreIds.Contains(w.Id)).Any(w => w.CleanName == f.CleanValue))); - } - - if (filter.Genres.Count > 0) - { - var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray(); - baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.Type == 2 && cleanGenres.Contains(f.CleanValue))); - } - - if (tags.Count > 0) - { - var cleanValues = tags.Select(e => GetCleanValue(e)).ToArray(); - baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.Type == 4 && cleanValues.Contains(f.CleanValue))); - } - - if (excludeTags.Count > 0) - { - var cleanValues = excludeTags.Select(e => GetCleanValue(e)).ToArray(); - baseQuery = baseQuery - .Where(e => !e.ItemValues!.Any(f => f.Type == 4 && cleanValues.Contains(f.CleanValue))); - } - - if (filter.StudioIds.Length > 0) - { - baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.Type == 3 && context.BaseItems.Where(w => filter.StudioIds.Contains(w.Id)).Any(w => w.CleanName == f.CleanValue))); - } - - if (filter.OfficialRatings.Length > 0) - { - baseQuery = baseQuery - .Where(e => filter.OfficialRatings.Contains(e.OfficialRating)); - } - - if (filter.HasParentalRating ?? false) - { - if (filter.MinParentalRating.HasValue) - { - baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue >= filter.MinParentalRating.Value); - } - - if (filter.MaxParentalRating.HasValue) - { - baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue < filter.MaxParentalRating.Value); - } - } - else if (filter.BlockUnratedItems.Length > 0) - { - if (filter.MinParentalRating.HasValue) - { - if (filter.MaxParentalRating.HasValue) - { - baseQuery = baseQuery - .Where(e => (e.InheritedParentalRatingValue == null && !filter.BlockUnratedItems.Select(e => e.ToString()).Contains(e.UnratedType)) - || (e.InheritedParentalRatingValue >= filter.MinParentalRating && e.InheritedParentalRatingValue <= filter.MaxParentalRating)); - } - else - { - baseQuery = baseQuery - .Where(e => (e.InheritedParentalRatingValue == null && !filter.BlockUnratedItems.Select(e => e.ToString()).Contains(e.UnratedType)) - || e.InheritedParentalRatingValue >= filter.MinParentalRating); - } - } - else - { - baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue != null && !filter.BlockUnratedItems.Select(e => e.ToString()).Contains(e.UnratedType)); - } - } - else if (filter.MinParentalRating.HasValue) - { - if (filter.MaxParentalRating.HasValue) - { - baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MinParentalRating.Value && e.InheritedParentalRatingValue <= filter.MaxParentalRating.Value); - } - else - { - baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MinParentalRating.Value); - } - } - else if (filter.MaxParentalRating.HasValue) - { - baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MaxParentalRating.Value); - } - else if (!filter.HasParentalRating ?? false) - { - baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue == null); - } - - if (filter.HasOfficialRating.HasValue) - { - if (filter.HasOfficialRating.Value) - { - baseQuery = baseQuery - .Where(e => e.OfficialRating != null && e.OfficialRating != string.Empty); - } - else - { - baseQuery = baseQuery - .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty); - } - } - - if (filter.HasOverview.HasValue) - { - if (filter.HasOverview.Value) - { - baseQuery = baseQuery - .Where(e => e.Overview != null && e.Overview != string.Empty); - } - else - { - baseQuery = baseQuery - .Where(e => e.Overview == null || e.Overview == string.Empty); - } - } - - if (filter.HasOwnerId.HasValue) - { - if (filter.HasOwnerId.Value) - { - baseQuery = baseQuery - .Where(e => e.OwnerId != null); - } - else - { - baseQuery = baseQuery - .Where(e => e.OwnerId == null); - } - } - - if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage)) - { - baseQuery = baseQuery - .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Audio" && e.Language == filter.HasNoAudioTrackWithLanguage)); - } - - if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage)) - { - baseQuery = baseQuery - .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Subtitle" && !e.IsExternal && e.Language == filter.HasNoInternalSubtitleTrackWithLanguage)); - } - - if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage)) - { - baseQuery = baseQuery - .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Subtitle" && e.IsExternal && e.Language == filter.HasNoExternalSubtitleTrackWithLanguage)); - } - - if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage)) - { - baseQuery = baseQuery - .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Subtitle" && e.Language == filter.HasNoSubtitleTrackWithLanguage)); - } - - if (filter.HasSubtitles.HasValue) - { - baseQuery = baseQuery - .Where(e => e.MediaStreams!.Any(e => e.StreamType == "Subtitle") == filter.HasSubtitles.Value); - } - - if (filter.HasChapterImages.HasValue) - { - baseQuery = baseQuery - .Where(e => e.Chapters!.Any(e => e.ImagePath != null) == filter.HasChapterImages.Value); - } - - if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value) - { - baseQuery = baseQuery - .Where(e => e.ParentId.HasValue && context.BaseItems.Any(f => f.Id.Equals(e.ParentId.Value))); - } - - if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value) - { - baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => (f.Type == 0 || f.Type == 1) && f.CleanValue == e.CleanName)); - } - - if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value) - { - baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.Type == 3 && f.CleanValue == e.CleanName)); - } - - if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value) - { - baseQuery = baseQuery - .Where(e => !e.Peoples!.Any(f => f.Name == e.Name)); - } - - if (filter.Years.Length == 1) - { - baseQuery = baseQuery - .Where(e => e.ProductionYear == filter.Years[0]); - } - else if (filter.Years.Length > 1) - { - baseQuery = baseQuery - .Where(e => filter.Years.Any(f => f == e.ProductionYear)); - } - - var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing; - if (isVirtualItem.HasValue) - { - baseQuery = baseQuery - .Where(e => e.IsVirtualItem == isVirtualItem.Value); - } - - if (filter.IsSpecialSeason.HasValue) - { - if (filter.IsSpecialSeason.Value) - { - baseQuery = baseQuery - .Where(e => e.IndexNumber == 0); - } - else - { - baseQuery = baseQuery - .Where(e => e.IndexNumber != 0); - } - } - - if (filter.IsUnaired.HasValue) - { - if (filter.IsUnaired.Value) - { - baseQuery = baseQuery - .Where(e => e.PremiereDate >= now); - } - else - { - baseQuery = baseQuery - .Where(e => e.PremiereDate < now); - } - } - - if (filter.MediaTypes.Length == 1) - { - baseQuery = baseQuery - .Where(e => e.MediaType == filter.MediaTypes[0].ToString()); - } - else if (filter.MediaTypes.Length > 1) - { - baseQuery = baseQuery - .Where(e => filter.MediaTypes.Select(f => f.ToString()).Contains(e.MediaType)); - } - - if (filter.ItemIds.Length > 0) - { - baseQuery = baseQuery - .Where(e => filter.ItemIds.Contains(e.Id)); - } - - if (filter.ExcludeItemIds.Length > 0) - { - baseQuery = baseQuery - .Where(e => !filter.ItemIds.Contains(e.Id)); - } - - if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0) - { - baseQuery = baseQuery.Where(e => !e.Provider!.All(f => !filter.ExcludeProviderIds.All(w => f.ProviderId == w.Key && f.ProviderValue == w.Value))); - } - - if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0) - { - baseQuery = baseQuery.Where(e => e.Provider!.Any(f => !filter.HasAnyProviderId.Any(w => f.ProviderId == w.Key && f.ProviderValue == w.Value))); - } - - if (filter.HasImdbId.HasValue) - { - baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "imdb")); - } - - if (filter.HasTmdbId.HasValue) - { - baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tmdb")); - } - - if (filter.HasTvdbId.HasValue) - { - baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tvdb")); - } - - var queryTopParentIds = filter.TopParentIds; - - if (queryTopParentIds.Length > 0) - { - var includedItemByNameTypes = GetItemByNameTypesInQuery(filter); - 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))); - } - else - { - baseQuery = baseQuery.Where(e => queryTopParentIds.Any(w => w.Equals(e.TopParentId!.Value))); - } - } - - if (filter.AncestorIds.Length > 0) - { - baseQuery = baseQuery.Where(e => e.AncestorIds!.Any(f => filter.AncestorIds.Contains(f.Id))); - } - - 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)))); - } - - if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey)) - { - baseQuery = baseQuery - .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey); - } - - if (filter.ExcludeInheritedTags.Length > 0) - { - baseQuery = baseQuery - .Where(e => !e.ItemValues!.Where(e => e.Type == 6) - .Any(f => filter.ExcludeInheritedTags.Contains(f.CleanValue))); - } - - if (filter.IncludeInheritedTags.Length > 0) - { - // Episodes do not store inherit tags from their parents in the database, and the tag may be still required by the client. - // In addtion to the tags for the episodes themselves, we need to manually query its parent (the season)'s tags as well. - if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode) - { - baseQuery = baseQuery - .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) - .Any(f => filter.IncludeInheritedTags.Contains(f.CleanValue)))); - } - - // A playlist should be accessible to its owner regardless of allowed tags. - else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist) - { - baseQuery = baseQuery - .Where(e => e.ItemValues!.Where(e => e.Type == 6) - .Any(f => filter.IncludeInheritedTags.Contains(f.CleanValue)) || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")); - // d ^^ this is stupid it hate this. - } - else - { - baseQuery = baseQuery - .Where(e => e.ItemValues!.Where(e => e.Type == 6) - .Any(f => filter.IncludeInheritedTags.Contains(f.CleanValue))); - } - } - - if (filter.SeriesStatuses.Length > 0) - { - baseQuery = baseQuery - .Where(e => filter.SeriesStatuses.Any(f => e.Data!.Contains(f.ToString(), StringComparison.InvariantCultureIgnoreCase))); - } - - if (filter.BoxSetLibraryFolders.Length > 0) - { - baseQuery = baseQuery - .Where(e => filter.BoxSetLibraryFolders.Any(f => e.Data!.Contains(f.ToString("N", CultureInfo.InvariantCulture), StringComparison.InvariantCultureIgnoreCase))); - } - - 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))); - } - - if (filter.Is3D.HasValue) - { - if (filter.Is3D.Value) - { - baseQuery = baseQuery - .Where(e => e.Data!.Contains("Video3DFormat", StringComparison.InvariantCultureIgnoreCase)); - } - else - { - baseQuery = baseQuery - .Where(e => !e.Data!.Contains("Video3DFormat", StringComparison.InvariantCultureIgnoreCase)); - } - } - - if (filter.IsPlaceHolder.HasValue) - { - if (filter.IsPlaceHolder.Value) - { - baseQuery = baseQuery - .Where(e => e.Data!.Contains("IsPlaceHolder\":true", StringComparison.InvariantCultureIgnoreCase)); - } - else - { - baseQuery = baseQuery - .Where(e => !e.Data!.Contains("IsPlaceHolder\":true", StringComparison.InvariantCultureIgnoreCase)); - } - } - - if (filter.HasSpecialFeature.HasValue) - { - if (filter.HasSpecialFeature.Value) - { - baseQuery = baseQuery - .Where(e => e.ExtraIds != null); - } - else - { - baseQuery = baseQuery - .Where(e => e.ExtraIds == null); - } - } - - if (filter.HasTrailer.HasValue || filter.HasThemeSong.HasValue || filter.HasThemeVideo.HasValue) - { - if (filter.HasTrailer.GetValueOrDefault() || filter.HasThemeSong.GetValueOrDefault() || filter.HasThemeVideo.GetValueOrDefault()) - { - baseQuery = baseQuery - .Where(e => e.ExtraIds != null); - } - else - { - baseQuery = baseQuery - .Where(e => e.ExtraIds == null); - } - } - - return baseQuery; - } - - /// - /// Gets the type. - /// - /// Name of the type. - /// Type. - /// typeName is null. - private static Type? GetType(string typeName) - { - ArgumentException.ThrowIfNullOrEmpty(typeName); - - return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies() - .Select(a => a.GetType(k)) - .FirstOrDefault(t => t is not null)); - } - - /// - public void SaveImages(BaseItem item) - { - ArgumentNullException.ThrowIfNull(item); - - var images = SerializeImages(item.ImageInfos); - using var db = _dbProvider.CreateDbContext(); - - db.BaseItems - .Where(e => e.Id.Equals(item.Id)) - .ExecuteUpdate(e => e.SetProperty(f => f.Images, images)); - } - - /// - public void SaveItems(IReadOnlyList items, CancellationToken cancellationToken) - { - UpdateOrInsertItems(items, cancellationToken); - } - - /// - public void UpdateOrInsertItems(IReadOnlyList items, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(items); - cancellationToken.ThrowIfCancellationRequested(); - - var itemsLen = items.Count; - var tuples = new (BaseItemDto Item, List? AncestorIds, BaseItemDto TopParent, string? UserDataKey, List InheritedTags)[itemsLen]; - for (int i = 0; i < itemsLen; i++) - { - var item = items[i]; - var ancestorIds = item.SupportsAncestors ? - item.GetAncestorIds().Distinct().ToList() : - null; - - var topParent = item.GetTopParent(); - - var userdataKey = item.GetUserDataKeys().FirstOrDefault(); - var inheritedTags = item.GetInheritedTags(); - - tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags); - } - - using var context = _dbProvider.CreateDbContext(); - foreach (var item in tuples) - { - var entity = Map(item.Item); - context.BaseItems.Add(entity); - - if (item.Item.SupportsAncestors && item.AncestorIds != null) - { - foreach (var ancestorId in item.AncestorIds) - { - context.AncestorIds.Add(new Data.Entities.AncestorId() - { - Item = entity, - AncestorIdText = ancestorId.ToString(), - Id = ancestorId - }); - } - } - - var itemValues = GetItemValuesToSave(item.Item, item.InheritedTags); - context.ItemValues.Where(e => e.ItemId.Equals(entity.Id)).ExecuteDelete(); - foreach (var itemValue in itemValues) - { - context.ItemValues.Add(new() - { - Item = entity, - Type = itemValue.MagicNumber, - Value = itemValue.Value, - CleanValue = GetCleanValue(itemValue.Value) - }); - } - } - - context.SaveChanges(true); - } - - /// - public BaseItemDto? RetrieveItem(Guid id) - { - if (id.IsEmpty()) - { - throw new ArgumentException("Guid can't be empty", nameof(id)); - } - - using var context = _dbProvider.CreateDbContext(); - var item = context.BaseItems.FirstOrDefault(e => e.Id.Equals(id)); - if (item is null) - { - return null; - } - - return DeserialiseBaseItem(item); - } - - /// - /// Maps a Entity to the DTO. - /// - /// The entity. - /// The dto base instance. - /// The dto to map. - public BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto) - { - dto.Id = entity.Id; - dto.ParentId = entity.ParentId.GetValueOrDefault(); - dto.Path = entity.Path; - dto.EndDate = entity.EndDate; - dto.CommunityRating = entity.CommunityRating; - dto.CustomRating = entity.CustomRating; - dto.IndexNumber = entity.IndexNumber; - dto.IsLocked = entity.IsLocked; - dto.Name = entity.Name; - dto.OfficialRating = entity.OfficialRating; - dto.Overview = entity.Overview; - dto.ParentIndexNumber = entity.ParentIndexNumber; - dto.PremiereDate = entity.PremiereDate; - dto.ProductionYear = entity.ProductionYear; - dto.SortName = entity.SortName; - dto.ForcedSortName = entity.ForcedSortName; - dto.RunTimeTicks = entity.RunTimeTicks; - dto.PreferredMetadataLanguage = entity.PreferredMetadataLanguage; - dto.PreferredMetadataCountryCode = entity.PreferredMetadataCountryCode; - dto.IsInMixedFolder = entity.IsInMixedFolder; - dto.InheritedParentalRatingValue = entity.InheritedParentalRatingValue; - dto.CriticRating = entity.CriticRating; - dto.PresentationUniqueKey = entity.PresentationUniqueKey; - dto.OriginalTitle = entity.OriginalTitle; - dto.Album = entity.Album; - dto.LUFS = entity.LUFS; - dto.NormalizationGain = entity.NormalizationGain; - dto.IsVirtualItem = entity.IsVirtualItem; - dto.ExternalSeriesId = entity.ExternalSeriesId; - dto.Tagline = entity.Tagline; - dto.TotalBitrate = entity.TotalBitrate; - dto.ExternalId = entity.ExternalId; - dto.Size = entity.Size; - dto.Genres = entity.Genres?.Split('|'); - dto.DateCreated = entity.DateCreated.GetValueOrDefault(); - dto.DateModified = entity.DateModified.GetValueOrDefault(); - dto.ChannelId = string.IsNullOrWhiteSpace(entity.ChannelId) ? Guid.Empty : Guid.Parse(entity.ChannelId); - dto.DateLastRefreshed = entity.DateLastRefreshed.GetValueOrDefault(); - dto.DateLastSaved = entity.DateLastSaved.GetValueOrDefault(); - dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : Guid.Parse(entity.OwnerId); - dto.Width = entity.Width.GetValueOrDefault(); - dto.Height = entity.Height.GetValueOrDefault(); - if (entity.Provider is not null) - { - dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue); - } - - if (entity.ExtraType is not null) - { - dto.ExtraType = Enum.Parse(entity.ExtraType); - } - - if (entity.LockedFields is not null) - { - List? fields = null; - foreach (var i in entity.LockedFields.AsSpan().Split('|')) - { - if (Enum.TryParse(i, true, out MetadataField parsedValue)) - { - (fields ??= new List()).Add(parsedValue); - } - } - - dto.LockedFields = fields?.ToArray() ?? Array.Empty(); - } - - if (entity.Audio is not null) - { - dto.Audio = Enum.Parse(entity.Audio); - } - - dto.ExtraIds = entity.ExtraIds?.Split('|').Select(e => Guid.Parse(e)).ToArray(); - dto.ProductionLocations = entity.ProductionLocations?.Split('|'); - dto.Studios = entity.Studios?.Split('|'); - dto.Tags = entity.Tags?.Split('|'); - - if (dto is IHasProgramAttributes hasProgramAttributes) - { - hasProgramAttributes.IsMovie = entity.IsMovie; - hasProgramAttributes.IsSeries = entity.IsSeries; - hasProgramAttributes.EpisodeTitle = entity.EpisodeTitle; - hasProgramAttributes.IsRepeat = entity.IsRepeat; - } - - if (dto is LiveTvChannel liveTvChannel) - { - liveTvChannel.ServiceName = entity.ExternalServiceId; - } - - if (dto is Trailer trailer) - { - List? types = null; - foreach (var i in entity.TrailerTypes.AsSpan().Split('|')) - { - if (Enum.TryParse(i, true, out TrailerType parsedValue)) - { - (types ??= new List()).Add(parsedValue); - } - } - - trailer.TrailerTypes = types?.ToArray() ?? Array.Empty(); - } - - if (dto is Video video) - { - video.PrimaryVersionId = entity.PrimaryVersionId; - } - - if (dto is IHasSeries hasSeriesName) - { - hasSeriesName.SeriesName = entity.SeriesName; - hasSeriesName.SeriesId = entity.SeriesId.GetValueOrDefault(); - hasSeriesName.SeriesPresentationUniqueKey = entity.SeriesPresentationUniqueKey; - } - - if (dto is Episode episode) - { - episode.SeasonName = entity.SeasonName; - episode.SeasonId = entity.SeasonId.GetValueOrDefault(); - } - - if (dto is IHasArtist hasArtists) - { - hasArtists.Artists = entity.Artists?.Split('|', StringSplitOptions.RemoveEmptyEntries); - } - - if (dto is IHasAlbumArtist hasAlbumArtists) - { - hasAlbumArtists.AlbumArtists = entity.AlbumArtists?.Split('|', StringSplitOptions.RemoveEmptyEntries); - } - - if (dto is LiveTvProgram program) - { - program.ShowId = entity.ShowId; - } - - if (entity.Images is not null) - { - dto.ImageInfos = DeserializeImages(entity.Images); - } - - // dto.Type = entity.Type; - // dto.Data = entity.Data; - // dto.MediaType = entity.MediaType; - if (dto is IHasStartDate hasStartDate) - { - hasStartDate.StartDate = entity.StartDate; - } - - // Fields that are present in the DB but are never actually used - // dto.UnratedType = entity.UnratedType; - // dto.TopParentId = entity.TopParentId; - // dto.CleanName = entity.CleanName; - // dto.UserDataKey = entity.UserDataKey; - - if (dto is Folder folder) - { - folder.DateLastMediaAdded = entity.DateLastMediaAdded; - } - - return dto; - } - - /// - /// Maps a Entity to the DTO. - /// - /// The entity. - /// The dto to map. - public BaseItemEntity Map(BaseItemDto dto) - { - var entity = new BaseItemEntity() - { - Type = dto.GetType().ToString(), - }; - entity.Id = dto.Id; - entity.ParentId = dto.ParentId; - entity.Path = GetPathToSave(dto.Path); - entity.EndDate = dto.EndDate.GetValueOrDefault(); - entity.CommunityRating = dto.CommunityRating; - entity.CustomRating = dto.CustomRating; - entity.IndexNumber = dto.IndexNumber; - entity.IsLocked = dto.IsLocked; - entity.Name = dto.Name; - entity.OfficialRating = dto.OfficialRating; - entity.Overview = dto.Overview; - entity.ParentIndexNumber = dto.ParentIndexNumber; - entity.PremiereDate = dto.PremiereDate; - entity.ProductionYear = dto.ProductionYear; - entity.SortName = dto.SortName; - entity.ForcedSortName = dto.ForcedSortName; - entity.RunTimeTicks = dto.RunTimeTicks; - entity.PreferredMetadataLanguage = dto.PreferredMetadataLanguage; - entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode; - entity.IsInMixedFolder = dto.IsInMixedFolder; - entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue; - entity.CriticRating = dto.CriticRating; - entity.PresentationUniqueKey = dto.PresentationUniqueKey; - entity.OriginalTitle = dto.OriginalTitle; - entity.Album = dto.Album; - entity.LUFS = dto.LUFS; - entity.NormalizationGain = dto.NormalizationGain; - entity.IsVirtualItem = dto.IsVirtualItem; - entity.ExternalSeriesId = dto.ExternalSeriesId; - entity.Tagline = dto.Tagline; - entity.TotalBitrate = dto.TotalBitrate; - entity.ExternalId = dto.ExternalId; - entity.Size = dto.Size; - entity.Genres = string.Join('|', dto.Genres); - entity.DateCreated = dto.DateCreated; - entity.DateModified = dto.DateModified; - entity.ChannelId = dto.ChannelId.ToString(); - entity.DateLastRefreshed = dto.DateLastRefreshed; - entity.DateLastSaved = dto.DateLastSaved; - entity.OwnerId = dto.OwnerId.ToString(); - entity.Width = dto.Width; - entity.Height = dto.Height; - entity.Provider = dto.ProviderIds.Select(e => new Data.Entities.BaseItemProvider() - { - Item = entity, - ProviderId = e.Key, - ProviderValue = e.Value - }).ToList(); - - entity.Audio = dto.Audio?.ToString(); - entity.ExtraType = dto.ExtraType?.ToString(); - - entity.ExtraIds = string.Join('|', dto.ExtraIds); - entity.ProductionLocations = string.Join('|', dto.ProductionLocations); - 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; - - if (dto is IHasProgramAttributes hasProgramAttributes) - { - entity.IsMovie = hasProgramAttributes.IsMovie; - entity.IsSeries = hasProgramAttributes.IsSeries; - entity.EpisodeTitle = hasProgramAttributes.EpisodeTitle; - entity.IsRepeat = hasProgramAttributes.IsRepeat; - } - - if (dto is LiveTvChannel liveTvChannel) - { - 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; - } - - if (dto is IHasSeries hasSeriesName) - { - entity.SeriesName = hasSeriesName.SeriesName; - entity.SeriesId = hasSeriesName.SeriesId; - entity.SeriesPresentationUniqueKey = hasSeriesName.SeriesPresentationUniqueKey; - } - - if (dto is Episode episode) - { - entity.SeasonName = episode.SeasonName; - entity.SeasonId = episode.SeasonId; - } - - if (dto is IHasArtist hasArtists) - { - entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists) : null; - } - - if (dto is IHasAlbumArtist hasAlbumArtists) - { - entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtists) : null; - } - - if (dto is LiveTvProgram program) - { - entity.ShowId = program.ShowId; - } - - if (dto.ImageInfos is not null) - { - entity.Images = SerializeImages(dto.ImageInfos); - } - - // dto.Type = entity.Type; - // dto.Data = entity.Data; - // dto.MediaType = entity.MediaType; - if (dto is IHasStartDate hasStartDate) - { - entity.StartDate = hasStartDate.StartDate; - } - - // Fields that are present in the DB but are never actually used - // dto.UnratedType = entity.UnratedType; - // dto.TopParentId = entity.TopParentId; - // dto.CleanName = entity.CleanName; - // dto.UserDataKey = entity.UserDataKey; - - if (dto is Folder folder) - { - entity.DateLastMediaAdded = folder.DateLastMediaAdded; - entity.IsFolder = folder.IsFolder; - } - - return entity; - } - - private IReadOnlyList GetItemValueNames(int[] itemValueTypes, IReadOnlyList withItemTypes, IReadOnlyList excludeItemTypes) - { - using var context = _dbProvider.CreateDbContext(); - - var query = context.ItemValues - .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)))); - } - - 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.DistinctBy(e => e.CleanValue); - return query.Select(e => e.CleanValue).ToImmutableArray(); - } - - private BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity) - { - var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialise unkown type."); - var dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialise unkown type."); - return Map(baseItemEntity, dto); - } - - private static void PrepareFilterQuery(InternalItemsQuery query) - { - if (query.Limit.HasValue && query.EnableGroupByMetadataKey) - { - query.Limit = query.Limit.Value + 4; - } - - if (query.IsResumable ?? false) - { - query.IsVirtualItem = false; - } - } - - private string GetCleanValue(string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return value; - } - - return value.RemoveDiacritics().ToLowerInvariant(); - } - - private List<(int MagicNumber, string Value)> GetItemValuesToSave(BaseItem item, List inheritedTags) - { - var list = new List<(int, string)>(); - - if (item is IHasArtist hasArtist) - { - list.AddRange(hasArtist.Artists.Select(i => (0, i))); - } - - if (item is IHasAlbumArtist hasAlbumArtist) - { - list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (1, i))); - } - - list.AddRange(item.Genres.Select(i => (2, i))); - list.AddRange(item.Studios.Select(i => (3, i))); - list.AddRange(item.Tags.Select(i => (4, i))); - - // keywords was 5 - - list.AddRange(inheritedTags.Select(i => (6, i))); - - // Remove all invalid values. - list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2)); - - return list; - } - - internal static string? SerializeProviderIds(Dictionary providerIds) - { - StringBuilder str = new StringBuilder(); - foreach (var i in providerIds) - { - // Ideally we shouldn't need this IsNullOrWhiteSpace check, - // but we're seeing some cases of bad data slip through - if (string.IsNullOrWhiteSpace(i.Value)) - { - continue; - } - - str.Append(i.Key) - .Append('=') - .Append(i.Value) - .Append('|'); - } - - if (str.Length == 0) - { - return null; - } - - str.Length -= 1; // Remove last | - return str.ToString(); - } - - internal static void DeserializeProviderIds(string value, IHasProviderIds item) - { - if (string.IsNullOrWhiteSpace(value)) - { - return; - } - - foreach (var part in value.SpanSplit('|')) - { - var providerDelimiterIndex = part.IndexOf('='); - // Don't let empty values through - if (providerDelimiterIndex != -1 && part.Length != providerDelimiterIndex + 1) - { - item.SetProviderId(part[..providerDelimiterIndex].ToString(), part[(providerDelimiterIndex + 1)..].ToString()); - } - } - } - - internal string? SerializeImages(ItemImageInfo[] images) - { - if (images.Length == 0) - { - return null; - } - - StringBuilder str = new StringBuilder(); - foreach (var i in images) - { - if (string.IsNullOrWhiteSpace(i.Path)) - { - continue; - } - - AppendItemImageInfo(str, i); - str.Append('|'); - } - - str.Length -= 1; // Remove last | - return str.ToString(); - } - - internal ItemImageInfo[] DeserializeImages(string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return Array.Empty(); - } - - // 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(); - } - - // 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('|', '\\')); - } - } - - private string? GetPathToSave(string path) - { - if (path is null) - { - return null; - } - - return _appHost.ReverseVirtualPath(path); - } - - private string RestorePath(string path) - { - return _appHost.ExpandVirtualPath(path); - } - - internal ItemImageInfo? ItemImageInfoFromValueString(ReadOnlySpan value) - { - const char Delimiter = '*'; - - var nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - return null; - } - - ReadOnlySpan path = value[..nextSegment]; - value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - return null; - } - - ReadOnlySpan dateModified = value[..nextSegment]; - value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - nextSegment = value.Length; - } - - ReadOnlySpan 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 widthSpan = value[..nextSegment]; - - value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - nextSegment = value.Length; - } - - ReadOnlySpan 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 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 GetItemByNameTypesInQuery(InternalItemsQuery query) - { - var list = new List(); - - if (IsTypeInQuery(BaseItemKind.Person, query)) - { - list.Add(typeof(Person).FullName!); - } - - if (IsTypeInQuery(BaseItemKind.Genre, query)) - { - list.Add(typeof(Genre).FullName!); - } - - if (IsTypeInQuery(BaseItemKind.MusicGenre, query)) - { - list.Add(typeof(MusicGenre).FullName!); - } - - if (IsTypeInQuery(BaseItemKind.MusicArtist, query)) - { - list.Add(typeof(MusicArtist).FullName!); - } - - if (IsTypeInQuery(BaseItemKind.Studio, query)) - { - list.Add(typeof(Studio).FullName!); - } - - return list; - } - - private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query) - { - if (query.ExcludeItemTypes.Contains(type)) - { - return false; - } - - return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type); - } - - private IQueryable Pageinate(IQueryable query, InternalItemsQuery filter) - { - if (filter.Limit.HasValue || filter.StartIndex.HasValue) - { - var offset = filter.StartIndex ?? 0; - - if (offset > 0) - { - query = query.Skip(offset); - } - - if (filter.Limit.HasValue) - { - query = query.Take(filter.Limit.Value); - } - } - - return query; - } - - private Expression> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query) - { -#pragma warning disable CS8603 // Possible null reference return. - return sortBy switch - { - 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.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.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), - ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.Type == 3).Select(f => f.CleanValue), - ItemSortBy.OfficialRating => e => e.InheritedParentalRatingValue, - // ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", - ItemSortBy.SeriesSortName => e => e.SeriesName, - // ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder", - ItemSortBy.Album => e => e.Album, - ItemSortBy.DateCreated => e => e.DateCreated, - ItemSortBy.PremiereDate => e => e.PremiereDate, - ItemSortBy.StartDate => e => e.StartDate, - ItemSortBy.Name => e => e.Name, - ItemSortBy.CommunityRating => e => e.CommunityRating, - ItemSortBy.ProductionYear => e => e.ProductionYear, - ItemSortBy.CriticRating => e => e.CriticRating, - ItemSortBy.VideoBitRate => e => e.TotalBitrate, - ItemSortBy.ParentIndexNumber => e => e.ParentIndexNumber, - ItemSortBy.IndexNumber => e => e.IndexNumber, - _ => e => e.SortName - }; -#pragma warning restore CS8603 // Possible null reference return. - - } - - private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query) - { - if (!query.GroupByPresentationUniqueKey) - { - return false; - } - - if (query.GroupBySeriesPresentationUniqueKey) - { - return false; - } - - if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey)) - { - return false; - } - - if (query.User is null) - { - return false; - } - - if (query.IncludeItemTypes.Length == 0) - { - return true; - } - - return query.IncludeItemTypes.Contains(BaseItemKind.Episode) - || query.IncludeItemTypes.Contains(BaseItemKind.Video) - || query.IncludeItemTypes.Contains(BaseItemKind.Movie) - || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo) - || query.IncludeItemTypes.Contains(BaseItemKind.Series) - || query.IncludeItemTypes.Contains(BaseItemKind.Season); - } - - private IQueryable ApplyOrder(IQueryable query, InternalItemsQuery filter) - { - var orderBy = filter.OrderBy; - bool hasSearch = !string.IsNullOrEmpty(filter.SearchTerm); - - if (hasSearch) - { - List<(ItemSortBy, SortOrder)> prepend = new List<(ItemSortBy, SortOrder)>(4); - if (hasSearch) - { - prepend.Add((ItemSortBy.SortName, SortOrder.Ascending)); - } - - orderBy = filter.OrderBy = [.. prepend, .. orderBy]; - } - else if (orderBy.Count == 0) - { - return query; - } - - foreach (var item in orderBy) - { - var expression = MapOrderByField(item.OrderBy, filter); - if (item.SortOrder == SortOrder.Ascending) - { - query = query.OrderBy(expression); - } - else - { - query = query.OrderByDescending(expression); - } - } - - return query; - } -} diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs new file mode 100644 index 0000000000..a3e617a211 --- /dev/null +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -0,0 +1,2233 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +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; +using MediaBrowser.Model.Querying; +using Microsoft.EntityFrameworkCore; +using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem; +using BaseItemEntity = Jellyfin.Data.Entities.BaseItemEntity; + +namespace Jellyfin.Server.Implementations.Item; + +/// +/// Handles all storage logic for BaseItems. +/// +/// +/// Initializes a new instance of the class. +/// +/// The db factory. +/// The Application host. +/// The static type lookup. +public sealed class BaseItemRepository(IDbContextFactory dbProvider, IServerApplicationHost appHost, IItemTypeLookup itemTypeLookup) + : IItemRepository, IDisposable +{ + /// + /// This holds all the types in the running assemblies + /// so that we can de-serialize properly when we don't have strong types. + /// + private static readonly ConcurrentDictionary _typeMap = new ConcurrentDictionary(); + private bool _disposed; + + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + } + + /// + public void DeleteItem(Guid id) + { + ArgumentNullException.ThrowIfNull(id.IsEmpty() ? null : id); + + 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.SaveChanges(); + transaction.Commit(); + } + + /// + public void UpdateInheritedValues() + { + using var context = dbProvider.CreateDbContext(); + using var transaction = context.Database.BeginTransaction(); + + context.ItemValues.Where(e => e.Type == 6).ExecuteDelete(); + context.ItemValues.AddRange(context.ItemValues.Where(e => e.Type == 4).Select(e => new Data.Entities.ItemValue() + { + CleanValue = e.CleanValue, + ItemId = e.ItemId, + Type = 6, + Value = e.Value, + Item = null! + })); + + context.ItemValues.AddRange( + context.AncestorIds.Where(e => e.AncestorIdText != null).Join(context.ItemValues.Where(e => e.Value != null && e.Type == 4), e => e.Id, e => e.ItemId, (e, f) => new Data.Entities.ItemValue() + { + CleanValue = f.CleanValue, + ItemId = e.ItemId, + Item = null!, + Type = 6, + Value = f.Value + })); + context.SaveChanges(); + + transaction.Commit(); + } + + /// + public IReadOnlyList GetItemIdsList(InternalItemsQuery filter) + { + ArgumentNullException.ThrowIfNull(filter); + PrepareFilterQuery(filter); + + using var context = dbProvider.CreateDbContext(); + var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter) + .DistinctBy(e => e.Id); + + var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter); + if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey) + { + dbQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).SelectMany(e => e); + } + + if (enableGroupByPresentationUniqueKey) + { + dbQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).SelectMany(e => e); + } + + if (filter.GroupBySeriesPresentationUniqueKey) + { + dbQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).SelectMany(e => e); + } + + dbQuery = ApplyOrder(dbQuery, filter); + + return Pageinate(dbQuery, filter).Select(e => e.Id).ToImmutableArray(); + } + + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery filter) + { + return GetItemValues(filter, [0, 1], typeof(MusicArtist).FullName!); + } + + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery filter) + { + return GetItemValues(filter, [0], typeof(MusicArtist).FullName!); + } + + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery filter) + { + return GetItemValues(filter, [1], typeof(MusicArtist).FullName!); + } + + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery filter) + { + return GetItemValues(filter, [3], typeof(Studio).FullName!); + } + + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery filter) + { + return GetItemValues(filter, [2], typeof(Genre).FullName!); + } + + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery filter) + { + return GetItemValues(filter, [2], typeof(MusicGenre).FullName!); + } + + /// + public IReadOnlyList GetStudioNames() + { + return GetItemValueNames([3], Array.Empty(), Array.Empty()); + } + + /// + public IReadOnlyList GetAllArtistNames() + { + return GetItemValueNames([0, 1], Array.Empty(), Array.Empty()); + } + + /// + public IReadOnlyList GetMusicGenreNames() + { + return GetItemValueNames( + [2], + new string[] + { + typeof(Audio).FullName!, + typeof(MusicVideo).FullName!, + typeof(MusicAlbum).FullName!, + typeof(MusicArtist).FullName! + }, + Array.Empty()); + } + + /// + public IReadOnlyList GetGenreNames() + { + return GetItemValueNames( + [2], + Array.Empty(), + new string[] + { + typeof(Audio).FullName!, + typeof(MusicVideo).FullName!, + typeof(MusicAlbum).FullName!, + typeof(MusicArtist).FullName! + }); + } + + /// + public QueryResult GetItems(InternalItemsQuery filter) + { + ArgumentNullException.ThrowIfNull(filter); + if (!filter.EnableTotalRecordCount || (!filter.Limit.HasValue && (filter.StartIndex ?? 0) == 0)) + { + var returnList = GetItemList(filter); + return new QueryResult( + filter.StartIndex, + returnList.Count, + returnList); + } + + PrepareFilterQuery(filter); + var result = new QueryResult(); + + using var context = dbProvider.CreateDbContext(); + var dbQuery = TranslateQuery(context.BaseItems, context, filter) + .DistinctBy(e => e.Id); + if (filter.EnableTotalRecordCount) + { + result.TotalRecordCount = dbQuery.Count(); + } + + if (filter.Limit.HasValue || filter.StartIndex.HasValue) + { + var offset = filter.StartIndex ?? 0; + + if (offset > 0) + { + dbQuery = dbQuery.Skip(offset); + } + + if (filter.Limit.HasValue) + { + dbQuery = dbQuery.Take(filter.Limit.Value); + } + } + + result.Items = dbQuery.ToList().Select(DeserialiseBaseItem).ToImmutableArray(); + result.StartIndex = filter.StartIndex ?? 0; + return result; + } + + /// + public IReadOnlyList GetItemList(InternalItemsQuery filter) + { + ArgumentNullException.ThrowIfNull(filter); + PrepareFilterQuery(filter); + + using var context = dbProvider.CreateDbContext(); + var dbQuery = TranslateQuery(context.BaseItems, context, filter) + .DistinctBy(e => e.Id); + if (filter.Limit.HasValue || filter.StartIndex.HasValue) + { + var offset = filter.StartIndex ?? 0; + + if (offset > 0) + { + dbQuery = dbQuery.Skip(offset); + } + + if (filter.Limit.HasValue) + { + dbQuery = dbQuery.Take(filter.Limit.Value); + } + } + + return dbQuery.ToList().Select(DeserialiseBaseItem).ToImmutableArray(); + } + + /// + public int GetCount(InternalItemsQuery filter) + { + ArgumentNullException.ThrowIfNull(filter); + // Hack for right now since we currently don't support filtering out these duplicates within a query + PrepareFilterQuery(filter); + + using var context = dbProvider.CreateDbContext(); + var dbQuery = TranslateQuery(context.BaseItems, context, filter); + + return dbQuery.Count(); + } + + private IQueryable TranslateQuery( + IQueryable baseQuery, + JellyfinDbContext context, + InternalItemsQuery filter) + { + var minWidth = filter.MinWidth; + var maxWidth = filter.MaxWidth; + var now = DateTime.UtcNow; + + if (filter.IsHD.HasValue) + { + const int Threshold = 1200; + if (filter.IsHD.Value) + { + minWidth = Threshold; + } + else + { + maxWidth = Threshold - 1; + } + } + + if (filter.Is4K.HasValue) + { + const int Threshold = 3800; + if (filter.Is4K.Value) + { + minWidth = Threshold; + } + else + { + maxWidth = Threshold - 1; + } + } + + if (minWidth.HasValue) + { + baseQuery = baseQuery.Where(e => e.Width >= minWidth); + } + + if (filter.MinHeight.HasValue) + { + baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight); + } + + if (maxWidth.HasValue) + { + baseQuery = baseQuery.Where(e => e.Width >= maxWidth); + } + + if (filter.MaxHeight.HasValue) + { + baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight); + } + + if (filter.IsLocked.HasValue) + { + baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked); + } + + var tags = filter.Tags.ToList(); + var excludeTags = filter.ExcludeTags.ToList(); + + if (filter.IsMovie == true) + { + if (filter.IncludeItemTypes.Length == 0 + || filter.IncludeItemTypes.Contains(BaseItemKind.Movie) + || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer)) + { + baseQuery = baseQuery.Where(e => e.IsMovie); + } + } + else if (filter.IsMovie.HasValue) + { + baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie); + } + + if (filter.IsSeries.HasValue) + { + baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries); + } + + if (filter.IsSports.HasValue) + { + if (filter.IsSports.Value) + { + tags.Add("Sports"); + } + else + { + excludeTags.Add("Sports"); + } + } + + if (filter.IsNews.HasValue) + { + if (filter.IsNews.Value) + { + tags.Add("News"); + } + else + { + excludeTags.Add("News"); + } + } + + if (filter.IsKids.HasValue) + { + if (filter.IsKids.Value) + { + tags.Add("Kids"); + } + else + { + excludeTags.Add("Kids"); + } + } + + 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))); + } + + if (filter.IsFolder.HasValue) + { + baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder); + } + + var includeTypes = filter.IncludeItemTypes; + // Only specify excluded types if no included types are specified + if (filter.IncludeItemTypes.Length == 0) + { + var excludeTypes = filter.ExcludeItemTypes; + if (excludeTypes.Length == 1) + { + if (itemTypeLookup.BaseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName)) + { + baseQuery = baseQuery.Where(e => e.Type != excludeTypeName); + } + } + else if (excludeTypes.Length > 1) + { + var excludeTypeName = new List(); + foreach (var excludeType in excludeTypes) + { + if (itemTypeLookup.BaseItemKindNames.TryGetValue(excludeType, out var baseItemKindName)) + { + excludeTypeName.Add(baseItemKindName!); + } + } + + baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type)); + } + } + else if (includeTypes.Length == 1) + { + if (itemTypeLookup.BaseItemKindNames.TryGetValue(includeTypes[0], out var includeTypeName)) + { + baseQuery = baseQuery.Where(e => e.Type == includeTypeName); + } + } + else if (includeTypes.Length > 1) + { + var includeTypeName = new List(); + foreach (var includeType in includeTypes) + { + if (itemTypeLookup.BaseItemKindNames.TryGetValue(includeType, out var baseItemKindName)) + { + includeTypeName.Add(baseItemKindName!); + } + } + + baseQuery = baseQuery.Where(e => includeTypeName.Contains(e.Type)); + } + + if (filter.ChannelIds.Count == 1) + { + 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)); + } + + if (!filter.ParentId.IsEmpty()) + { + baseQuery = baseQuery.Where(e => e.ParentId.Equals(filter.ParentId)); + } + + if (!string.IsNullOrWhiteSpace(filter.Path)) + { + baseQuery = baseQuery.Where(e => e.Path == filter.Path); + } + + if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey)) + { + baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey); + } + + if (filter.MinCommunityRating.HasValue) + { + baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating); + } + + if (filter.MinIndexNumber.HasValue) + { + baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber); + } + + if (filter.MinParentAndIndexNumber.HasValue) + { + baseQuery = baseQuery + .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNumber >= filter.MinParentAndIndexNumber.Value.IndexNumber) || e.ParentIndexNumber > filter.MinParentAndIndexNumber.Value.ParentIndexNumber); + } + + if (filter.MinDateCreated.HasValue) + { + baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated); + } + + if (filter.MinDateLastSaved.HasValue) + { + baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value); + } + + if (filter.MinDateLastSavedForUser.HasValue) + { + baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUser.Value); + } + + if (filter.IndexNumber.HasValue) + { + baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value); + } + + if (filter.ParentIndexNumber.HasValue) + { + baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value); + } + + if (filter.ParentIndexNumberNotEquals.HasValue) + { + baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentIndexNumber == null); + } + + var minEndDate = filter.MinEndDate; + var maxEndDate = filter.MaxEndDate; + + if (filter.HasAired.HasValue) + { + if (filter.HasAired.Value) + { + maxEndDate = DateTime.UtcNow; + } + else + { + minEndDate = DateTime.UtcNow; + } + } + + if (minEndDate.HasValue) + { + baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate); + } + + if (maxEndDate.HasValue) + { + baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate); + } + + if (filter.MinStartDate.HasValue) + { + baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value); + } + + if (filter.MaxStartDate.HasValue) + { + baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value); + } + + if (filter.MinPremiereDate.HasValue) + { + baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MinPremiereDate.Value); + } + + if (filter.MaxPremiereDate.HasValue) + { + baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value); + } + + if (filter.TrailerTypes.Length > 0) + { + baseQuery = baseQuery.Where(e => filter.TrailerTypes.Any(f => e.TrailerTypes!.Contains(f.ToString(), StringComparison.OrdinalIgnoreCase))); + } + + if (filter.IsAiring.HasValue) + { + if (filter.IsAiring.Value) + { + baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now); + } + else + { + baseQuery = baseQuery.Where(e => e.StartDate > now && e.EndDate < now); + } + } + + if (filter.PersonIds.Length > 0) + { + 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))); + } + + if (!string.IsNullOrWhiteSpace(filter.Person)) + { + baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.Name == filter.Person)); + } + + if (!string.IsNullOrWhiteSpace(filter.MinSortName)) + { + // this does not makes sense. + // baseQuery = baseQuery.Where(e => e.SortName >= query.MinSortName); + // whereClauses.Add("SortName>=@MinSortName"); + // statement?.TryBind("@MinSortName", query.MinSortName); + } + + if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId)) + { + baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId); + } + + if (!string.IsNullOrWhiteSpace(filter.ExternalId)) + { + baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId); + } + + if (!string.IsNullOrWhiteSpace(filter.Name)) + { + var cleanName = GetCleanValue(filter.Name); + baseQuery = baseQuery.Where(e => e.CleanName == cleanName); + } + + // These are the same, for now + var nameContains = filter.NameContains; + if (!string.IsNullOrWhiteSpace(nameContains)) + { + baseQuery = baseQuery.Where(e => + e.CleanName == filter.NameContains + || e.OriginalTitle!.Contains(filter.NameContains!, StringComparison.Ordinal)); + } + + if (!string.IsNullOrWhiteSpace(filter.NameStartsWith)) + { + baseQuery = baseQuery.Where(e => e.SortName!.Contains(filter.NameStartsWith, StringComparison.OrdinalIgnoreCase)); + } + + if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater)) + { + // i hate this + baseQuery = baseQuery.Where(e => e.SortName![0] > filter.NameStartsWithOrGreater[0]); + } + + if (!string.IsNullOrWhiteSpace(filter.NameLessThan)) + { + // i hate this + baseQuery = baseQuery.Where(e => e.SortName![0] < filter.NameLessThan[0]); + } + + if (filter.ImageTypes.Length > 0) + { + baseQuery = baseQuery.Where(e => filter.ImageTypes.Any(f => e.Images!.Contains(f.ToString(), StringComparison.InvariantCulture))); + } + + 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); + } + + 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); + } + + 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); + } + + 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); + } + + if (filter.IsResumable.HasValue) + { + if (filter.IsResumable.Value) + { + baseQuery = baseQuery + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(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); + } + } + + var artistQuery = context.BaseItems.Where(w => filter.ArtistIds.Contains(w.Id)); + + if (filter.ArtistIds.Length > 0) + { + baseQuery = baseQuery + .Where(e => e.ItemValues!.Any(f => f.Type <= 1 && artistQuery.Any(w => w.CleanName == f.CleanValue))); + } + + if (filter.AlbumArtistIds.Length > 0) + { + baseQuery = baseQuery + .Where(e => e.ItemValues!.Any(f => f.Type == 1 && artistQuery.Any(w => w.CleanName == f.CleanValue))); + } + + if (filter.ContributingArtistIds.Length > 0) + { + var contributingArtists = context.BaseItems.Where(e => filter.ContributingArtistIds.Contains(e.Id)); + baseQuery = baseQuery.Where(e => e.ItemValues!.Any(f => f.Type == 0 && contributingArtists.Any(w => w.CleanName == f.CleanValue))); + } + + if (filter.AlbumIds.Length > 0) + { + baseQuery = baseQuery.Where(e => context.BaseItems.Where(e => filter.AlbumIds.Contains(e.Id)).Any(f => f.Name == e.Album)); + } + + if (filter.ExcludeArtistIds.Length > 0) + { + var excludeArtistQuery = context.BaseItems.Where(w => filter.ExcludeArtistIds.Contains(w.Id)); + baseQuery = baseQuery + .Where(e => !e.ItemValues!.Any(f => f.Type <= 1 && artistQuery.Any(w => w.CleanName == f.CleanValue))); + } + + if (filter.GenreIds.Count > 0) + { + baseQuery = baseQuery + .Where(e => e.ItemValues!.Any(f => f.Type == 2 && context.BaseItems.Where(w => filter.GenreIds.Contains(w.Id)).Any(w => w.CleanName == f.CleanValue))); + } + + if (filter.Genres.Count > 0) + { + var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray(); + baseQuery = baseQuery + .Where(e => e.ItemValues!.Any(f => f.Type == 2 && cleanGenres.Contains(f.CleanValue))); + } + + if (tags.Count > 0) + { + var cleanValues = tags.Select(e => GetCleanValue(e)).ToArray(); + baseQuery = baseQuery + .Where(e => e.ItemValues!.Any(f => f.Type == 4 && cleanValues.Contains(f.CleanValue))); + } + + if (excludeTags.Count > 0) + { + var cleanValues = excludeTags.Select(e => GetCleanValue(e)).ToArray(); + baseQuery = baseQuery + .Where(e => !e.ItemValues!.Any(f => f.Type == 4 && cleanValues.Contains(f.CleanValue))); + } + + if (filter.StudioIds.Length > 0) + { + baseQuery = baseQuery + .Where(e => e.ItemValues!.Any(f => f.Type == 3 && context.BaseItems.Where(w => filter.StudioIds.Contains(w.Id)).Any(w => w.CleanName == f.CleanValue))); + } + + if (filter.OfficialRatings.Length > 0) + { + baseQuery = baseQuery + .Where(e => filter.OfficialRatings.Contains(e.OfficialRating)); + } + + if (filter.HasParentalRating ?? false) + { + if (filter.MinParentalRating.HasValue) + { + baseQuery = baseQuery + .Where(e => e.InheritedParentalRatingValue >= filter.MinParentalRating.Value); + } + + if (filter.MaxParentalRating.HasValue) + { + baseQuery = baseQuery + .Where(e => e.InheritedParentalRatingValue < filter.MaxParentalRating.Value); + } + } + else if (filter.BlockUnratedItems.Length > 0) + { + if (filter.MinParentalRating.HasValue) + { + if (filter.MaxParentalRating.HasValue) + { + baseQuery = baseQuery + .Where(e => (e.InheritedParentalRatingValue == null && !filter.BlockUnratedItems.Select(e => e.ToString()).Contains(e.UnratedType)) + || (e.InheritedParentalRatingValue >= filter.MinParentalRating && e.InheritedParentalRatingValue <= filter.MaxParentalRating)); + } + else + { + baseQuery = baseQuery + .Where(e => (e.InheritedParentalRatingValue == null && !filter.BlockUnratedItems.Select(e => e.ToString()).Contains(e.UnratedType)) + || e.InheritedParentalRatingValue >= filter.MinParentalRating); + } + } + else + { + baseQuery = baseQuery + .Where(e => e.InheritedParentalRatingValue != null && !filter.BlockUnratedItems.Select(e => e.ToString()).Contains(e.UnratedType)); + } + } + else if (filter.MinParentalRating.HasValue) + { + if (filter.MaxParentalRating.HasValue) + { + baseQuery = baseQuery + .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MinParentalRating.Value && e.InheritedParentalRatingValue <= filter.MaxParentalRating.Value); + } + else + { + baseQuery = baseQuery + .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MinParentalRating.Value); + } + } + else if (filter.MaxParentalRating.HasValue) + { + baseQuery = baseQuery + .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MaxParentalRating.Value); + } + else if (!filter.HasParentalRating ?? false) + { + baseQuery = baseQuery + .Where(e => e.InheritedParentalRatingValue == null); + } + + if (filter.HasOfficialRating.HasValue) + { + if (filter.HasOfficialRating.Value) + { + baseQuery = baseQuery + .Where(e => e.OfficialRating != null && e.OfficialRating != string.Empty); + } + else + { + baseQuery = baseQuery + .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty); + } + } + + if (filter.HasOverview.HasValue) + { + if (filter.HasOverview.Value) + { + baseQuery = baseQuery + .Where(e => e.Overview != null && e.Overview != string.Empty); + } + else + { + baseQuery = baseQuery + .Where(e => e.Overview == null || e.Overview == string.Empty); + } + } + + if (filter.HasOwnerId.HasValue) + { + if (filter.HasOwnerId.Value) + { + baseQuery = baseQuery + .Where(e => e.OwnerId != null); + } + else + { + baseQuery = baseQuery + .Where(e => e.OwnerId == null); + } + } + + if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage)) + { + baseQuery = baseQuery + .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Audio" && e.Language == filter.HasNoAudioTrackWithLanguage)); + } + + if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage)) + { + baseQuery = baseQuery + .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Subtitle" && !e.IsExternal && e.Language == filter.HasNoInternalSubtitleTrackWithLanguage)); + } + + if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage)) + { + baseQuery = baseQuery + .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Subtitle" && e.IsExternal && e.Language == filter.HasNoExternalSubtitleTrackWithLanguage)); + } + + if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage)) + { + baseQuery = baseQuery + .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Subtitle" && e.Language == filter.HasNoSubtitleTrackWithLanguage)); + } + + if (filter.HasSubtitles.HasValue) + { + baseQuery = baseQuery + .Where(e => e.MediaStreams!.Any(e => e.StreamType == "Subtitle") == filter.HasSubtitles.Value); + } + + if (filter.HasChapterImages.HasValue) + { + baseQuery = baseQuery + .Where(e => e.Chapters!.Any(e => e.ImagePath != null) == filter.HasChapterImages.Value); + } + + if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value) + { + baseQuery = baseQuery + .Where(e => e.ParentId.HasValue && context.BaseItems.Any(f => f.Id.Equals(e.ParentId.Value))); + } + + if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value) + { + baseQuery = baseQuery + .Where(e => e.ItemValues!.Any(f => (f.Type == 0 || f.Type == 1) && f.CleanValue == e.CleanName)); + } + + if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value) + { + baseQuery = baseQuery + .Where(e => e.ItemValues!.Any(f => f.Type == 3 && f.CleanValue == e.CleanName)); + } + + if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value) + { + baseQuery = baseQuery + .Where(e => !e.Peoples!.Any(f => f.Name == e.Name)); + } + + if (filter.Years.Length == 1) + { + baseQuery = baseQuery + .Where(e => e.ProductionYear == filter.Years[0]); + } + else if (filter.Years.Length > 1) + { + baseQuery = baseQuery + .Where(e => filter.Years.Any(f => f == e.ProductionYear)); + } + + var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing; + if (isVirtualItem.HasValue) + { + baseQuery = baseQuery + .Where(e => e.IsVirtualItem == isVirtualItem.Value); + } + + if (filter.IsSpecialSeason.HasValue) + { + if (filter.IsSpecialSeason.Value) + { + baseQuery = baseQuery + .Where(e => e.IndexNumber == 0); + } + else + { + baseQuery = baseQuery + .Where(e => e.IndexNumber != 0); + } + } + + if (filter.IsUnaired.HasValue) + { + if (filter.IsUnaired.Value) + { + baseQuery = baseQuery + .Where(e => e.PremiereDate >= now); + } + else + { + baseQuery = baseQuery + .Where(e => e.PremiereDate < now); + } + } + + if (filter.MediaTypes.Length == 1) + { + baseQuery = baseQuery + .Where(e => e.MediaType == filter.MediaTypes[0].ToString()); + } + else if (filter.MediaTypes.Length > 1) + { + baseQuery = baseQuery + .Where(e => filter.MediaTypes.Select(f => f.ToString()).Contains(e.MediaType)); + } + + if (filter.ItemIds.Length > 0) + { + baseQuery = baseQuery + .Where(e => filter.ItemIds.Contains(e.Id)); + } + + if (filter.ExcludeItemIds.Length > 0) + { + baseQuery = baseQuery + .Where(e => !filter.ItemIds.Contains(e.Id)); + } + + if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0) + { + baseQuery = baseQuery.Where(e => !e.Provider!.All(f => !filter.ExcludeProviderIds.All(w => f.ProviderId == w.Key && f.ProviderValue == w.Value))); + } + + if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0) + { + baseQuery = baseQuery.Where(e => e.Provider!.Any(f => !filter.HasAnyProviderId.Any(w => f.ProviderId == w.Key && f.ProviderValue == w.Value))); + } + + if (filter.HasImdbId.HasValue) + { + baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "imdb")); + } + + if (filter.HasTmdbId.HasValue) + { + baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tmdb")); + } + + if (filter.HasTvdbId.HasValue) + { + baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tvdb")); + } + + var queryTopParentIds = filter.TopParentIds; + + if (queryTopParentIds.Length > 0) + { + var includedItemByNameTypes = GetItemByNameTypesInQuery(filter); + 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))); + } + else + { + baseQuery = baseQuery.Where(e => queryTopParentIds.Any(w => w.Equals(e.TopParentId!.Value))); + } + } + + if (filter.AncestorIds.Length > 0) + { + baseQuery = baseQuery.Where(e => e.AncestorIds!.Any(f => filter.AncestorIds.Contains(f.Id))); + } + + 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)))); + } + + if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey)) + { + baseQuery = baseQuery + .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey); + } + + if (filter.ExcludeInheritedTags.Length > 0) + { + baseQuery = baseQuery + .Where(e => !e.ItemValues!.Where(e => e.Type == 6) + .Any(f => filter.ExcludeInheritedTags.Contains(f.CleanValue))); + } + + if (filter.IncludeInheritedTags.Length > 0) + { + // Episodes do not store inherit tags from their parents in the database, and the tag may be still required by the client. + // In addtion to the tags for the episodes themselves, we need to manually query its parent (the season)'s tags as well. + if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode) + { + baseQuery = baseQuery + .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) + .Any(f => filter.IncludeInheritedTags.Contains(f.CleanValue)))); + } + + // A playlist should be accessible to its owner regardless of allowed tags. + else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist) + { + baseQuery = baseQuery + .Where(e => e.ItemValues!.Where(e => e.Type == 6) + .Any(f => filter.IncludeInheritedTags.Contains(f.CleanValue)) || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")); + // d ^^ this is stupid it hate this. + } + else + { + baseQuery = baseQuery + .Where(e => e.ItemValues!.Where(e => e.Type == 6) + .Any(f => filter.IncludeInheritedTags.Contains(f.CleanValue))); + } + } + + if (filter.SeriesStatuses.Length > 0) + { + baseQuery = baseQuery + .Where(e => filter.SeriesStatuses.Any(f => e.Data!.Contains(f.ToString(), StringComparison.InvariantCultureIgnoreCase))); + } + + if (filter.BoxSetLibraryFolders.Length > 0) + { + baseQuery = baseQuery + .Where(e => filter.BoxSetLibraryFolders.Any(f => e.Data!.Contains(f.ToString("N", CultureInfo.InvariantCulture), StringComparison.InvariantCultureIgnoreCase))); + } + + 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))); + } + + if (filter.Is3D.HasValue) + { + if (filter.Is3D.Value) + { + baseQuery = baseQuery + .Where(e => e.Data!.Contains("Video3DFormat", StringComparison.InvariantCultureIgnoreCase)); + } + else + { + baseQuery = baseQuery + .Where(e => !e.Data!.Contains("Video3DFormat", StringComparison.InvariantCultureIgnoreCase)); + } + } + + if (filter.IsPlaceHolder.HasValue) + { + if (filter.IsPlaceHolder.Value) + { + baseQuery = baseQuery + .Where(e => e.Data!.Contains("IsPlaceHolder\":true", StringComparison.InvariantCultureIgnoreCase)); + } + else + { + baseQuery = baseQuery + .Where(e => !e.Data!.Contains("IsPlaceHolder\":true", StringComparison.InvariantCultureIgnoreCase)); + } + } + + if (filter.HasSpecialFeature.HasValue) + { + if (filter.HasSpecialFeature.Value) + { + baseQuery = baseQuery + .Where(e => e.ExtraIds != null); + } + else + { + baseQuery = baseQuery + .Where(e => e.ExtraIds == null); + } + } + + if (filter.HasTrailer.HasValue || filter.HasThemeSong.HasValue || filter.HasThemeVideo.HasValue) + { + if (filter.HasTrailer.GetValueOrDefault() || filter.HasThemeSong.GetValueOrDefault() || filter.HasThemeVideo.GetValueOrDefault()) + { + baseQuery = baseQuery + .Where(e => e.ExtraIds != null); + } + else + { + baseQuery = baseQuery + .Where(e => e.ExtraIds == null); + } + } + + return baseQuery; + } + + /// + /// Gets the type. + /// + /// Name of the type. + /// Type. + /// typeName is null. + private static Type? GetType(string typeName) + { + ArgumentException.ThrowIfNullOrEmpty(typeName); + + return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies() + .Select(a => a.GetType(k)) + .FirstOrDefault(t => t is not null)); + } + + /// + public void SaveImages(BaseItem item) + { + ArgumentNullException.ThrowIfNull(item); + + var images = SerializeImages(item.ImageInfos); + using var db = dbProvider.CreateDbContext(); + + db.BaseItems + .Where(e => e.Id.Equals(item.Id)) + .ExecuteUpdate(e => e.SetProperty(f => f.Images, images)); + } + + /// + public void SaveItems(IReadOnlyList items, CancellationToken cancellationToken) + { + UpdateOrInsertItems(items, cancellationToken); + } + + /// + public void UpdateOrInsertItems(IReadOnlyList items, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(items); + cancellationToken.ThrowIfCancellationRequested(); + + var itemsLen = items.Count; + var tuples = new (BaseItemDto Item, List? AncestorIds, BaseItemDto TopParent, string? UserDataKey, List InheritedTags)[itemsLen]; + for (int i = 0; i < itemsLen; i++) + { + var item = items[i]; + var ancestorIds = item.SupportsAncestors ? + item.GetAncestorIds().Distinct().ToList() : + null; + + var topParent = item.GetTopParent(); + + var userdataKey = item.GetUserDataKeys().FirstOrDefault(); + var inheritedTags = item.GetInheritedTags(); + + tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags); + } + + using var context = dbProvider.CreateDbContext(); + foreach (var item in tuples) + { + var entity = Map(item.Item); + context.BaseItems.Add(entity); + + if (item.Item.SupportsAncestors && item.AncestorIds != null) + { + foreach (var ancestorId in item.AncestorIds) + { + context.AncestorIds.Add(new Data.Entities.AncestorId() + { + Item = entity, + AncestorIdText = ancestorId.ToString(), + Id = ancestorId + }); + } + } + + var itemValues = GetItemValuesToSave(item.Item, item.InheritedTags); + context.ItemValues.Where(e => e.ItemId.Equals(entity.Id)).ExecuteDelete(); + foreach (var itemValue in itemValues) + { + context.ItemValues.Add(new() + { + Item = entity, + Type = itemValue.MagicNumber, + Value = itemValue.Value, + CleanValue = GetCleanValue(itemValue.Value) + }); + } + } + + context.SaveChanges(true); + } + + /// + public BaseItemDto? RetrieveItem(Guid id) + { + if (id.IsEmpty()) + { + throw new ArgumentException("Guid can't be empty", nameof(id)); + } + + using var context = dbProvider.CreateDbContext(); + var item = context.BaseItems.FirstOrDefault(e => e.Id.Equals(id)); + if (item is null) + { + return null; + } + + return DeserialiseBaseItem(item); + } + + /// + /// Maps a Entity to the DTO. + /// + /// The entity. + /// The dto base instance. + /// The dto to map. + public BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto) + { + dto.Id = entity.Id; + dto.ParentId = entity.ParentId.GetValueOrDefault(); + dto.Path = entity.Path; + dto.EndDate = entity.EndDate; + dto.CommunityRating = entity.CommunityRating; + dto.CustomRating = entity.CustomRating; + dto.IndexNumber = entity.IndexNumber; + dto.IsLocked = entity.IsLocked; + dto.Name = entity.Name; + dto.OfficialRating = entity.OfficialRating; + dto.Overview = entity.Overview; + dto.ParentIndexNumber = entity.ParentIndexNumber; + dto.PremiereDate = entity.PremiereDate; + dto.ProductionYear = entity.ProductionYear; + dto.SortName = entity.SortName; + dto.ForcedSortName = entity.ForcedSortName; + dto.RunTimeTicks = entity.RunTimeTicks; + dto.PreferredMetadataLanguage = entity.PreferredMetadataLanguage; + dto.PreferredMetadataCountryCode = entity.PreferredMetadataCountryCode; + dto.IsInMixedFolder = entity.IsInMixedFolder; + dto.InheritedParentalRatingValue = entity.InheritedParentalRatingValue; + dto.CriticRating = entity.CriticRating; + dto.PresentationUniqueKey = entity.PresentationUniqueKey; + dto.OriginalTitle = entity.OriginalTitle; + dto.Album = entity.Album; + dto.LUFS = entity.LUFS; + dto.NormalizationGain = entity.NormalizationGain; + dto.IsVirtualItem = entity.IsVirtualItem; + dto.ExternalSeriesId = entity.ExternalSeriesId; + dto.Tagline = entity.Tagline; + dto.TotalBitrate = entity.TotalBitrate; + dto.ExternalId = entity.ExternalId; + dto.Size = entity.Size; + dto.Genres = entity.Genres?.Split('|'); + dto.DateCreated = entity.DateCreated.GetValueOrDefault(); + dto.DateModified = entity.DateModified.GetValueOrDefault(); + dto.ChannelId = string.IsNullOrWhiteSpace(entity.ChannelId) ? Guid.Empty : Guid.Parse(entity.ChannelId); + dto.DateLastRefreshed = entity.DateLastRefreshed.GetValueOrDefault(); + dto.DateLastSaved = entity.DateLastSaved.GetValueOrDefault(); + dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : Guid.Parse(entity.OwnerId); + dto.Width = entity.Width.GetValueOrDefault(); + dto.Height = entity.Height.GetValueOrDefault(); + if (entity.Provider is not null) + { + dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue); + } + + if (entity.ExtraType is not null) + { + dto.ExtraType = Enum.Parse(entity.ExtraType); + } + + if (entity.LockedFields is not null) + { + List? fields = null; + foreach (var i in entity.LockedFields.AsSpan().Split('|')) + { + if (Enum.TryParse(i, true, out MetadataField parsedValue)) + { + (fields ??= new List()).Add(parsedValue); + } + } + + dto.LockedFields = fields?.ToArray() ?? Array.Empty(); + } + + if (entity.Audio is not null) + { + dto.Audio = Enum.Parse(entity.Audio); + } + + dto.ExtraIds = entity.ExtraIds?.Split('|').Select(e => Guid.Parse(e)).ToArray(); + dto.ProductionLocations = entity.ProductionLocations?.Split('|'); + dto.Studios = entity.Studios?.Split('|'); + dto.Tags = entity.Tags?.Split('|'); + + if (dto is IHasProgramAttributes hasProgramAttributes) + { + hasProgramAttributes.IsMovie = entity.IsMovie; + hasProgramAttributes.IsSeries = entity.IsSeries; + hasProgramAttributes.EpisodeTitle = entity.EpisodeTitle; + hasProgramAttributes.IsRepeat = entity.IsRepeat; + } + + if (dto is LiveTvChannel liveTvChannel) + { + liveTvChannel.ServiceName = entity.ExternalServiceId; + } + + if (dto is Trailer trailer) + { + List? types = null; + foreach (var i in entity.TrailerTypes.AsSpan().Split('|')) + { + if (Enum.TryParse(i, true, out TrailerType parsedValue)) + { + (types ??= new List()).Add(parsedValue); + } + } + + trailer.TrailerTypes = types?.ToArray() ?? Array.Empty(); + } + + if (dto is Video video) + { + video.PrimaryVersionId = entity.PrimaryVersionId; + } + + if (dto is IHasSeries hasSeriesName) + { + hasSeriesName.SeriesName = entity.SeriesName; + hasSeriesName.SeriesId = entity.SeriesId.GetValueOrDefault(); + hasSeriesName.SeriesPresentationUniqueKey = entity.SeriesPresentationUniqueKey; + } + + if (dto is Episode episode) + { + episode.SeasonName = entity.SeasonName; + episode.SeasonId = entity.SeasonId.GetValueOrDefault(); + } + + if (dto is IHasArtist hasArtists) + { + hasArtists.Artists = entity.Artists?.Split('|', StringSplitOptions.RemoveEmptyEntries); + } + + if (dto is IHasAlbumArtist hasAlbumArtists) + { + hasAlbumArtists.AlbumArtists = entity.AlbumArtists?.Split('|', StringSplitOptions.RemoveEmptyEntries); + } + + if (dto is LiveTvProgram program) + { + program.ShowId = entity.ShowId; + } + + if (entity.Images is not null) + { + dto.ImageInfos = DeserializeImages(entity.Images); + } + + // dto.Type = entity.Type; + // dto.Data = entity.Data; + // dto.MediaType = entity.MediaType; + if (dto is IHasStartDate hasStartDate) + { + hasStartDate.StartDate = entity.StartDate; + } + + // Fields that are present in the DB but are never actually used + // dto.UnratedType = entity.UnratedType; + // dto.TopParentId = entity.TopParentId; + // dto.CleanName = entity.CleanName; + // dto.UserDataKey = entity.UserDataKey; + + if (dto is Folder folder) + { + folder.DateLastMediaAdded = entity.DateLastMediaAdded; + } + + return dto; + } + + /// + /// Maps a Entity to the DTO. + /// + /// The entity. + /// The dto to map. + public BaseItemEntity Map(BaseItemDto dto) + { + var entity = new BaseItemEntity() + { + Type = dto.GetType().ToString(), + }; + entity.Id = dto.Id; + entity.ParentId = dto.ParentId; + entity.Path = GetPathToSave(dto.Path); + entity.EndDate = dto.EndDate.GetValueOrDefault(); + entity.CommunityRating = dto.CommunityRating; + entity.CustomRating = dto.CustomRating; + entity.IndexNumber = dto.IndexNumber; + entity.IsLocked = dto.IsLocked; + entity.Name = dto.Name; + entity.OfficialRating = dto.OfficialRating; + entity.Overview = dto.Overview; + entity.ParentIndexNumber = dto.ParentIndexNumber; + entity.PremiereDate = dto.PremiereDate; + entity.ProductionYear = dto.ProductionYear; + entity.SortName = dto.SortName; + entity.ForcedSortName = dto.ForcedSortName; + entity.RunTimeTicks = dto.RunTimeTicks; + entity.PreferredMetadataLanguage = dto.PreferredMetadataLanguage; + entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode; + entity.IsInMixedFolder = dto.IsInMixedFolder; + entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue; + entity.CriticRating = dto.CriticRating; + entity.PresentationUniqueKey = dto.PresentationUniqueKey; + entity.OriginalTitle = dto.OriginalTitle; + entity.Album = dto.Album; + entity.LUFS = dto.LUFS; + entity.NormalizationGain = dto.NormalizationGain; + entity.IsVirtualItem = dto.IsVirtualItem; + entity.ExternalSeriesId = dto.ExternalSeriesId; + entity.Tagline = dto.Tagline; + entity.TotalBitrate = dto.TotalBitrate; + entity.ExternalId = dto.ExternalId; + entity.Size = dto.Size; + entity.Genres = string.Join('|', dto.Genres); + entity.DateCreated = dto.DateCreated; + entity.DateModified = dto.DateModified; + entity.ChannelId = dto.ChannelId.ToString(); + entity.DateLastRefreshed = dto.DateLastRefreshed; + entity.DateLastSaved = dto.DateLastSaved; + entity.OwnerId = dto.OwnerId.ToString(); + entity.Width = dto.Width; + entity.Height = dto.Height; + entity.Provider = dto.ProviderIds.Select(e => new Data.Entities.BaseItemProvider() + { + Item = entity, + ProviderId = e.Key, + ProviderValue = e.Value + }).ToList(); + + entity.Audio = dto.Audio?.ToString(); + entity.ExtraType = dto.ExtraType?.ToString(); + + entity.ExtraIds = string.Join('|', dto.ExtraIds); + entity.ProductionLocations = string.Join('|', dto.ProductionLocations); + 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; + + if (dto is IHasProgramAttributes hasProgramAttributes) + { + entity.IsMovie = hasProgramAttributes.IsMovie; + entity.IsSeries = hasProgramAttributes.IsSeries; + entity.EpisodeTitle = hasProgramAttributes.EpisodeTitle; + entity.IsRepeat = hasProgramAttributes.IsRepeat; + } + + if (dto is LiveTvChannel liveTvChannel) + { + 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; + } + + if (dto is IHasSeries hasSeriesName) + { + entity.SeriesName = hasSeriesName.SeriesName; + entity.SeriesId = hasSeriesName.SeriesId; + entity.SeriesPresentationUniqueKey = hasSeriesName.SeriesPresentationUniqueKey; + } + + if (dto is Episode episode) + { + entity.SeasonName = episode.SeasonName; + entity.SeasonId = episode.SeasonId; + } + + if (dto is IHasArtist hasArtists) + { + entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists) : null; + } + + if (dto is IHasAlbumArtist hasAlbumArtists) + { + entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtists) : null; + } + + if (dto is LiveTvProgram program) + { + entity.ShowId = program.ShowId; + } + + if (dto.ImageInfos is not null) + { + entity.Images = SerializeImages(dto.ImageInfos); + } + + // dto.Type = entity.Type; + // dto.Data = entity.Data; + // dto.MediaType = entity.MediaType; + if (dto is IHasStartDate hasStartDate) + { + entity.StartDate = hasStartDate.StartDate; + } + + // Fields that are present in the DB but are never actually used + // dto.UnratedType = entity.UnratedType; + // dto.TopParentId = entity.TopParentId; + // dto.CleanName = entity.CleanName; + // dto.UserDataKey = entity.UserDataKey; + + if (dto is Folder folder) + { + entity.DateLastMediaAdded = folder.DateLastMediaAdded; + entity.IsFolder = folder.IsFolder; + } + + return entity; + } + + private IReadOnlyList GetItemValueNames(int[] itemValueTypes, IReadOnlyList withItemTypes, IReadOnlyList excludeItemTypes) + { + using var context = dbProvider.CreateDbContext(); + + var query = context.ItemValues + .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)))); + } + + 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.DistinctBy(e => e.CleanValue); + return query.Select(e => e.CleanValue).ToImmutableArray(); + } + + private BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity) + { + var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialise unkown type."); + var dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialise unkown type."); + return Map(baseItemEntity, dto); + } + + private QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetItemValues(InternalItemsQuery filter, int[] itemValueTypes, string returnType) + { + ArgumentNullException.ThrowIfNull(filter); + + if (!filter.Limit.HasValue) + { + filter.EnableTotalRecordCount = false; + } + + using var context = dbProvider.CreateDbContext(); + + var innerQuery = new InternalItemsQuery(filter.User) + { + ExcludeItemTypes = filter.ExcludeItemTypes, + IncludeItemTypes = filter.IncludeItemTypes, + MediaTypes = filter.MediaTypes, + AncestorIds = filter.AncestorIds, + ItemIds = filter.ItemIds, + TopParentIds = filter.TopParentIds, + ParentId = filter.ParentId, + IsAiring = filter.IsAiring, + IsMovie = filter.IsMovie, + IsSports = filter.IsSports, + IsKids = filter.IsKids, + IsNews = filter.IsNews, + IsSeries = filter.IsSeries + }; + var query = TranslateQuery(context.BaseItems, context, innerQuery); + + query = query.Where(e => e.Type == returnType && e.ItemValues!.Any(f => e.CleanName == f.CleanValue && itemValueTypes.Contains(f.Type))); + + var outerQuery = new InternalItemsQuery(filter.User) + { + IsPlayed = filter.IsPlayed, + IsFavorite = filter.IsFavorite, + IsFavoriteOrLiked = filter.IsFavoriteOrLiked, + IsLiked = filter.IsLiked, + IsLocked = filter.IsLocked, + NameLessThan = filter.NameLessThan, + NameStartsWith = filter.NameStartsWith, + NameStartsWithOrGreater = filter.NameStartsWithOrGreater, + Tags = filter.Tags, + OfficialRatings = filter.OfficialRatings, + StudioIds = filter.StudioIds, + GenreIds = filter.GenreIds, + Genres = filter.Genres, + Years = filter.Years, + NameContains = filter.NameContains, + SearchTerm = filter.SearchTerm, + SimilarTo = filter.SimilarTo, + ExcludeItemIds = filter.ExcludeItemIds + }; + query = TranslateQuery(query, context, outerQuery) + .OrderBy(e => e.PresentationUniqueKey); + + if (filter.OrderBy.Count != 0 + || filter.SimilarTo is not null + || !string.IsNullOrEmpty(filter.SearchTerm)) + { + query = ApplyOrder(query, filter); + } + else + { + query = query.OrderBy(e => e.SortName); + } + + if (filter.Limit.HasValue || filter.StartIndex.HasValue) + { + var offset = filter.StartIndex ?? 0; + + if (offset > 0) + { + query = query.Skip(offset); + } + + if (filter.Limit.HasValue) + { + query.Take(filter.Limit.Value); + } + } + + var result = new QueryResult<(BaseItem, ItemCounts)>(); + string countText = string.Empty; + if (filter.EnableTotalRecordCount) + { + result.TotalRecordCount = query.DistinctBy(e => e.PresentationUniqueKey).Count(); + } + + var resultQuery = query.Select(e => new + { + item = e, + itemCount = new ItemCounts() + { + SeriesCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Series), + EpisodeCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Episode), + MovieCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Movie), + AlbumCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.MusicAlbum), + ArtistCount = e.ItemValues!.Count(e => e.Type == 0 || e.Type == 1), + SongCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.MusicAlbum), + TrailerCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Trailer), + } + }); + + result.StartIndex = filter.StartIndex ?? 0; + result.Items = resultQuery.ToImmutableArray().Select(e => + { + return (DeserialiseBaseItem(e.item), e.itemCount); + }).ToImmutableArray(); + + return result; + } + + private static void PrepareFilterQuery(InternalItemsQuery query) + { + if (query.Limit.HasValue && query.EnableGroupByMetadataKey) + { + query.Limit = query.Limit.Value + 4; + } + + if (query.IsResumable ?? false) + { + query.IsVirtualItem = false; + } + } + + private string GetCleanValue(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return value; + } + + return value.RemoveDiacritics().ToLowerInvariant(); + } + + private List<(int MagicNumber, string Value)> GetItemValuesToSave(BaseItem item, List inheritedTags) + { + var list = new List<(int, string)>(); + + if (item is IHasArtist hasArtist) + { + list.AddRange(hasArtist.Artists.Select(i => (0, i))); + } + + if (item is IHasAlbumArtist hasAlbumArtist) + { + list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (1, i))); + } + + list.AddRange(item.Genres.Select(i => (2, i))); + list.AddRange(item.Studios.Select(i => (3, i))); + list.AddRange(item.Tags.Select(i => (4, i))); + + // keywords was 5 + + list.AddRange(inheritedTags.Select(i => (6, i))); + + // Remove all invalid values. + list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2)); + + return list; + } + + internal static string? SerializeProviderIds(Dictionary providerIds) + { + StringBuilder str = new StringBuilder(); + foreach (var i in providerIds) + { + // Ideally we shouldn't need this IsNullOrWhiteSpace check, + // but we're seeing some cases of bad data slip through + if (string.IsNullOrWhiteSpace(i.Value)) + { + continue; + } + + str.Append(i.Key) + .Append('=') + .Append(i.Value) + .Append('|'); + } + + if (str.Length == 0) + { + return null; + } + + str.Length -= 1; // Remove last | + return str.ToString(); + } + + internal static void DeserializeProviderIds(string value, IHasProviderIds item) + { + if (string.IsNullOrWhiteSpace(value)) + { + return; + } + + foreach (var part in value.SpanSplit('|')) + { + var providerDelimiterIndex = part.IndexOf('='); + // Don't let empty values through + if (providerDelimiterIndex != -1 && part.Length != providerDelimiterIndex + 1) + { + item.SetProviderId(part[..providerDelimiterIndex].ToString(), part[(providerDelimiterIndex + 1)..].ToString()); + } + } + } + + internal string? SerializeImages(ItemImageInfo[] images) + { + if (images.Length == 0) + { + return null; + } + + StringBuilder str = new StringBuilder(); + foreach (var i in images) + { + if (string.IsNullOrWhiteSpace(i.Path)) + { + continue; + } + + AppendItemImageInfo(str, i); + str.Append('|'); + } + + str.Length -= 1; // Remove last | + return str.ToString(); + } + + internal ItemImageInfo[] DeserializeImages(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return Array.Empty(); + } + + // 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(); + } + + // 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('|', '\\')); + } + } + + private string? GetPathToSave(string path) + { + if (path is null) + { + return null; + } + + return appHost.ReverseVirtualPath(path); + } + + private string RestorePath(string path) + { + return appHost.ExpandVirtualPath(path); + } + + internal ItemImageInfo? ItemImageInfoFromValueString(ReadOnlySpan value) + { + const char Delimiter = '*'; + + var nextSegment = value.IndexOf(Delimiter); + if (nextSegment == -1) + { + return null; + } + + ReadOnlySpan path = value[..nextSegment]; + value = value[(nextSegment + 1)..]; + nextSegment = value.IndexOf(Delimiter); + if (nextSegment == -1) + { + return null; + } + + ReadOnlySpan dateModified = value[..nextSegment]; + value = value[(nextSegment + 1)..]; + nextSegment = value.IndexOf(Delimiter); + if (nextSegment == -1) + { + nextSegment = value.Length; + } + + ReadOnlySpan 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 widthSpan = value[..nextSegment]; + + value = value[(nextSegment + 1)..]; + nextSegment = value.IndexOf(Delimiter); + if (nextSegment == -1) + { + nextSegment = value.Length; + } + + ReadOnlySpan 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 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 GetItemByNameTypesInQuery(InternalItemsQuery query) + { + var list = new List(); + + if (IsTypeInQuery(BaseItemKind.Person, query)) + { + list.Add(typeof(Person).FullName!); + } + + if (IsTypeInQuery(BaseItemKind.Genre, query)) + { + list.Add(typeof(Genre).FullName!); + } + + if (IsTypeInQuery(BaseItemKind.MusicGenre, query)) + { + list.Add(typeof(MusicGenre).FullName!); + } + + if (IsTypeInQuery(BaseItemKind.MusicArtist, query)) + { + list.Add(typeof(MusicArtist).FullName!); + } + + if (IsTypeInQuery(BaseItemKind.Studio, query)) + { + list.Add(typeof(Studio).FullName!); + } + + return list; + } + + private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query) + { + if (query.ExcludeItemTypes.Contains(type)) + { + return false; + } + + return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type); + } + + private IQueryable Pageinate(IQueryable query, InternalItemsQuery filter) + { + if (filter.Limit.HasValue || filter.StartIndex.HasValue) + { + var offset = filter.StartIndex ?? 0; + + if (offset > 0) + { + query = query.Skip(offset); + } + + if (filter.Limit.HasValue) + { + query = query.Take(filter.Limit.Value); + } + } + + return query; + } + + private Expression> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query) + { +#pragma warning disable CS8603 // Possible null reference return. + return sortBy switch + { + 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.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.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), + ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.Type == 3).Select(f => f.CleanValue), + ItemSortBy.OfficialRating => e => e.InheritedParentalRatingValue, + // ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", + ItemSortBy.SeriesSortName => e => e.SeriesName, + // ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder", + ItemSortBy.Album => e => e.Album, + ItemSortBy.DateCreated => e => e.DateCreated, + ItemSortBy.PremiereDate => e => e.PremiereDate, + ItemSortBy.StartDate => e => e.StartDate, + ItemSortBy.Name => e => e.Name, + ItemSortBy.CommunityRating => e => e.CommunityRating, + ItemSortBy.ProductionYear => e => e.ProductionYear, + ItemSortBy.CriticRating => e => e.CriticRating, + ItemSortBy.VideoBitRate => e => e.TotalBitrate, + ItemSortBy.ParentIndexNumber => e => e.ParentIndexNumber, + ItemSortBy.IndexNumber => e => e.IndexNumber, + _ => e => e.SortName + }; +#pragma warning restore CS8603 // Possible null reference return. + + } + + private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query) + { + if (!query.GroupByPresentationUniqueKey) + { + return false; + } + + if (query.GroupBySeriesPresentationUniqueKey) + { + return false; + } + + if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey)) + { + return false; + } + + if (query.User is null) + { + return false; + } + + if (query.IncludeItemTypes.Length == 0) + { + return true; + } + + return query.IncludeItemTypes.Contains(BaseItemKind.Episode) + || query.IncludeItemTypes.Contains(BaseItemKind.Video) + || query.IncludeItemTypes.Contains(BaseItemKind.Movie) + || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo) + || query.IncludeItemTypes.Contains(BaseItemKind.Series) + || query.IncludeItemTypes.Contains(BaseItemKind.Season); + } + + private IQueryable ApplyOrder(IQueryable query, InternalItemsQuery filter) + { + var orderBy = filter.OrderBy; + bool hasSearch = !string.IsNullOrEmpty(filter.SearchTerm); + + if (hasSearch) + { + List<(ItemSortBy, SortOrder)> prepend = new List<(ItemSortBy, SortOrder)>(4); + if (hasSearch) + { + prepend.Add((ItemSortBy.SortName, SortOrder.Ascending)); + } + + orderBy = filter.OrderBy = [.. prepend, .. orderBy]; + } + else if (orderBy.Count == 0) + { + return query; + } + + foreach (var item in orderBy) + { + var expression = MapOrderByField(item.OrderBy, filter); + if (item.SortOrder == SortOrder.Ascending) + { + query = query.OrderBy(expression); + } + else + { + query = query.OrderByDescending(expression); + } + } + + return query; + } +} diff --git a/Jellyfin.Server.Implementations/Item/ChapterManager.cs b/Jellyfin.Server.Implementations/Item/ChapterManager.cs deleted file mode 100644 index 7b0f98fde5..0000000000 --- a/Jellyfin.Server.Implementations/Item/ChapterManager.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using Jellyfin.Data.Entities; -using MediaBrowser.Controller.Chapters; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Jellyfin.Server.Implementations.Item; - -/// -/// The Chapter manager. -/// -public class ChapterManager : IChapterManager -{ - private readonly IDbContextFactory _dbProvider; - private readonly IImageProcessor _imageProcessor; - - /// - /// Initializes a new instance of the class. - /// - /// The EFCore provider. - /// The Image Processor. - public ChapterManager(IDbContextFactory dbProvider, IImageProcessor imageProcessor) - { - _dbProvider = dbProvider; - _imageProcessor = imageProcessor; - } - - /// - public ChapterInfo? GetChapter(BaseItemDto baseItem, int index) - { - using var context = _dbProvider.CreateDbContext(); - var chapter = context.Chapters.FirstOrDefault(e => e.ItemId.Equals(baseItem.Id) && e.ChapterIndex == index); - if (chapter is not null) - { - return Map(chapter, baseItem); - } - - return null; - } - - /// - public IReadOnlyList GetChapters(BaseItemDto baseItem) - { - using var context = _dbProvider.CreateDbContext(); - return context.Chapters.Where(e => e.ItemId.Equals(baseItem.Id)) - .ToList() - .Select(e => Map(e, baseItem)) - .ToImmutableArray(); - } - - /// - public void SaveChapters(Guid itemId, IReadOnlyList chapters) - { - using var context = _dbProvider.CreateDbContext(); - using (var transaction = context.Database.BeginTransaction()) - { - context.Chapters.Where(e => e.ItemId.Equals(itemId)).ExecuteDelete(); - for (var i = 0; i < chapters.Count; i++) - { - var chapter = chapters[i]; - context.Chapters.Add(Map(chapter, i, itemId)); - } - - context.SaveChanges(); - transaction.Commit(); - } - } - - private Chapter Map(ChapterInfo chapterInfo, int index, Guid itemId) - { - return new Chapter() - { - ChapterIndex = index, - StartPositionTicks = chapterInfo.StartPositionTicks, - ImageDateModified = chapterInfo.ImageDateModified, - ImagePath = chapterInfo.ImagePath, - ItemId = itemId, - Name = chapterInfo.Name - }; - } - - private ChapterInfo Map(Chapter chapterInfo, BaseItemDto baseItem) - { - var info = new ChapterInfo() - { - StartPositionTicks = chapterInfo.StartPositionTicks, - ImageDateModified = chapterInfo.ImageDateModified.GetValueOrDefault(), - ImagePath = chapterInfo.ImagePath, - Name = chapterInfo.Name, - }; - info.ImageTag = _imageProcessor.GetImageCacheTag(baseItem, info); - return info; - } -} diff --git a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs new file mode 100644 index 0000000000..d215a1d7ad --- /dev/null +++ b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller.Chapters; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Jellyfin.Server.Implementations.Item; + +/// +/// The Chapter manager. +/// +public class ChapterRepository : IChapterRepository +{ + private readonly IDbContextFactory _dbProvider; + private readonly IImageProcessor _imageProcessor; + + /// + /// Initializes a new instance of the class. + /// + /// The EFCore provider. + /// The Image Processor. + public ChapterRepository(IDbContextFactory dbProvider, IImageProcessor imageProcessor) + { + _dbProvider = dbProvider; + _imageProcessor = imageProcessor; + } + + /// + public ChapterInfo? GetChapter(BaseItemDto baseItem, int index) + { + return GetChapter(baseItem.Id, index); + } + + /// + public IReadOnlyList GetChapters(BaseItemDto baseItem) + { + return GetChapters(baseItem.Id); + } + + /// + public ChapterInfo? GetChapter(Guid baseItemId, int index) + { + using var context = _dbProvider.CreateDbContext(); + var chapter = context.Chapters + .Select(e => new + { + chapter = e, + baseItemPath = e.Item.Path + }) + .FirstOrDefault(e => e.chapter.ItemId.Equals(baseItemId) && e.chapter.ChapterIndex == index); + if (chapter is not null) + { + return Map(chapter.chapter, chapter.baseItemPath!); + } + + return null; + } + + /// + public IReadOnlyList GetChapters(Guid baseItemId) + { + using var context = _dbProvider.CreateDbContext(); + return context.Chapters.Where(e => e.ItemId.Equals(baseItemId)) + .Select(e => new + { + chapter = e, + baseItemPath = e.Item.Path + }) + .ToList() + .Select(e => Map(e.chapter, e.baseItemPath!)) + .ToImmutableArray(); + } + + /// + public void SaveChapters(Guid itemId, IReadOnlyList chapters) + { + using var context = _dbProvider.CreateDbContext(); + using (var transaction = context.Database.BeginTransaction()) + { + context.Chapters.Where(e => e.ItemId.Equals(itemId)).ExecuteDelete(); + for (var i = 0; i < chapters.Count; i++) + { + var chapter = chapters[i]; + context.Chapters.Add(Map(chapter, i, itemId)); + } + + context.SaveChanges(); + transaction.Commit(); + } + } + + private Chapter Map(ChapterInfo chapterInfo, int index, Guid itemId) + { + return new Chapter() + { + ChapterIndex = index, + StartPositionTicks = chapterInfo.StartPositionTicks, + ImageDateModified = chapterInfo.ImageDateModified, + ImagePath = chapterInfo.ImagePath, + ItemId = itemId, + Name = chapterInfo.Name, + Item = null! + }; + } + + private ChapterInfo Map(Chapter chapterInfo, string baseItemPath) + { + var chapterEntity = new ChapterInfo() + { + StartPositionTicks = chapterInfo.StartPositionTicks, + ImageDateModified = chapterInfo.ImageDateModified.GetValueOrDefault(), + ImagePath = chapterInfo.ImagePath, + Name = chapterInfo.Name, + }; + chapterEntity.ImageTag = _imageProcessor.GetImageCacheTag(baseItemPath, chapterEntity.ImageDateModified); + return chapterEntity; + } +} diff --git a/Jellyfin.Server.Implementations/Item/MediaAttachmentManager.cs b/Jellyfin.Server.Implementations/Item/MediaAttachmentManager.cs deleted file mode 100644 index 288b1943e7..0000000000 --- a/Jellyfin.Server.Implementations/Item/MediaAttachmentManager.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using Jellyfin.Data.Entities; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Jellyfin.Server.Implementations.Item; - -/// -/// Manager for handling Media Attachments. -/// -/// Efcore Factory. -public class MediaAttachmentManager(IDbContextFactory dbProvider) : IMediaAttachmentManager -{ - /// - public void SaveMediaAttachments( - Guid id, - IReadOnlyList attachments, - CancellationToken cancellationToken) - { - using var context = dbProvider.CreateDbContext(); - using var transaction = context.Database.BeginTransaction(); - context.AttachmentStreamInfos.Where(e => e.ItemId.Equals(id)).ExecuteDelete(); - context.AttachmentStreamInfos.AddRange(attachments.Select(e => Map(e, id))); - context.SaveChanges(); - transaction.Commit(); - } - - /// - public IReadOnlyList GetMediaAttachments(MediaAttachmentQuery filter) - { - using var context = dbProvider.CreateDbContext(); - var query = context.AttachmentStreamInfos.Where(e => e.ItemId.Equals(filter.ItemId)); - if (filter.Index.HasValue) - { - query = query.Where(e => e.Index == filter.Index); - } - - return query.ToList().Select(Map).ToImmutableArray(); - } - - private MediaAttachment Map(AttachmentStreamInfo attachment) - { - return new MediaAttachment() - { - Codec = attachment.Codec, - CodecTag = attachment.CodecTag, - Comment = attachment.Comment, - FileName = attachment.Filename, - Index = attachment.Index, - MimeType = attachment.MimeType, - }; - } - - private AttachmentStreamInfo Map(MediaAttachment attachment, Guid id) - { - return new AttachmentStreamInfo() - { - Codec = attachment.Codec, - CodecTag = attachment.CodecTag, - Comment = attachment.Comment, - Filename = attachment.FileName, - Index = attachment.Index, - MimeType = attachment.MimeType, - ItemId = id, - Item = null! - }; - } -} diff --git a/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs b/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs new file mode 100644 index 0000000000..70c5ff1e2e --- /dev/null +++ b/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Jellyfin.Server.Implementations.Item; + +/// +/// Manager for handling Media Attachments. +/// +/// Efcore Factory. +public class MediaAttachmentRepository(IDbContextFactory dbProvider) : IMediaAttachmentRepository +{ + /// + public void SaveMediaAttachments( + Guid id, + IReadOnlyList attachments, + CancellationToken cancellationToken) + { + using var context = dbProvider.CreateDbContext(); + using var transaction = context.Database.BeginTransaction(); + context.AttachmentStreamInfos.Where(e => e.ItemId.Equals(id)).ExecuteDelete(); + context.AttachmentStreamInfos.AddRange(attachments.Select(e => Map(e, id))); + context.SaveChanges(); + transaction.Commit(); + } + + /// + public IReadOnlyList GetMediaAttachments(MediaAttachmentQuery filter) + { + using var context = dbProvider.CreateDbContext(); + var query = context.AttachmentStreamInfos.Where(e => e.ItemId.Equals(filter.ItemId)); + if (filter.Index.HasValue) + { + query = query.Where(e => e.Index == filter.Index); + } + + return query.ToList().Select(Map).ToImmutableArray(); + } + + private MediaAttachment Map(AttachmentStreamInfo attachment) + { + return new MediaAttachment() + { + Codec = attachment.Codec, + CodecTag = attachment.CodecTag, + Comment = attachment.Comment, + FileName = attachment.Filename, + Index = attachment.Index, + MimeType = attachment.MimeType, + }; + } + + private AttachmentStreamInfo Map(MediaAttachment attachment, Guid id) + { + return new AttachmentStreamInfo() + { + Codec = attachment.Codec, + CodecTag = attachment.CodecTag, + Comment = attachment.Comment, + Filename = attachment.FileName, + Index = attachment.Index, + MimeType = attachment.MimeType, + ItemId = id, + Item = null! + }; + } +} diff --git a/Jellyfin.Server.Implementations/Item/MediaStreamManager.cs b/Jellyfin.Server.Implementations/Item/MediaStreamManager.cs deleted file mode 100644 index b7124283a4..0000000000 --- a/Jellyfin.Server.Implementations/Item/MediaStreamManager.cs +++ /dev/null @@ -1,201 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using Jellyfin.Data.Entities; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using Microsoft.EntityFrameworkCore; - -namespace Jellyfin.Server.Implementations.Item; - -/// -/// Initializes a new instance of the class. -/// -/// -/// -/// -public class MediaStreamManager(IDbContextFactory dbProvider, IServerApplicationHost serverApplicationHost, ILocalizationManager localization) : IMediaStreamManager -{ - /// - public void SaveMediaStreams(Guid id, IReadOnlyList streams, CancellationToken cancellationToken) - { - using var context = dbProvider.CreateDbContext(); - using var transaction = context.Database.BeginTransaction(); - - context.MediaStreamInfos.Where(e => e.ItemId.Equals(id)).ExecuteDelete(); - context.MediaStreamInfos.AddRange(streams.Select(f => Map(f, id))); - context.SaveChanges(); - - transaction.Commit(); - } - - /// - public IReadOnlyList GetMediaStreams(MediaStreamQuery filter) - { - using var context = dbProvider.CreateDbContext(); - return TranslateQuery(context.MediaStreamInfos, filter).ToList().Select(Map).ToImmutableArray(); - } - - private string? GetPathToSave(string? path) - { - if (path is null) - { - return null; - } - - return serverApplicationHost.ReverseVirtualPath(path); - } - - private string? RestorePath(string? path) - { - if (path is null) - { - return null; - } - - return serverApplicationHost.ExpandVirtualPath(path); - } - - private IQueryable TranslateQuery(IQueryable query, MediaStreamQuery filter) - { - query = query.Where(e => e.ItemId.Equals(filter.ItemId)); - if (filter.Index.HasValue) - { - query = query.Where(e => e.StreamIndex == filter.Index); - } - - if (filter.Type.HasValue) - { - query = query.Where(e => e.StreamType == filter.Type.ToString()); - } - - return query; - } - - private MediaStream Map(MediaStreamInfo entity) - { - var dto = new MediaStream(); - dto.Index = entity.StreamIndex; - if (entity.StreamType != null) - { - dto.Type = Enum.Parse(entity.StreamType); - } - - dto.IsAVC = entity.IsAvc; - dto.Codec = entity.Codec; - dto.Language = entity.Language; - dto.ChannelLayout = entity.ChannelLayout; - dto.Profile = entity.Profile; - dto.AspectRatio = entity.AspectRatio; - dto.Path = RestorePath(entity.Path); - dto.IsInterlaced = entity.IsInterlaced; - dto.BitRate = entity.BitRate; - dto.Channels = entity.Channels; - dto.SampleRate = entity.SampleRate; - dto.IsDefault = entity.IsDefault; - dto.IsForced = entity.IsForced; - dto.IsExternal = entity.IsExternal; - dto.Height = entity.Height; - dto.Width = entity.Width; - dto.AverageFrameRate = entity.AverageFrameRate; - dto.RealFrameRate = entity.RealFrameRate; - dto.Level = entity.Level; - dto.PixelFormat = entity.PixelFormat; - dto.BitDepth = entity.BitDepth; - dto.IsAnamorphic = entity.IsAnamorphic; - dto.RefFrames = entity.RefFrames; - dto.CodecTag = entity.CodecTag; - dto.Comment = entity.Comment; - dto.NalLengthSize = entity.NalLengthSize; - dto.Title = entity.Title; - dto.TimeBase = entity.TimeBase; - dto.CodecTimeBase = entity.CodecTimeBase; - dto.ColorPrimaries = entity.ColorPrimaries; - dto.ColorSpace = entity.ColorSpace; - dto.ColorTransfer = entity.ColorTransfer; - dto.DvVersionMajor = entity.DvVersionMajor; - dto.DvVersionMinor = entity.DvVersionMinor; - dto.DvProfile = entity.DvProfile; - dto.DvLevel = entity.DvLevel; - dto.RpuPresentFlag = entity.RpuPresentFlag; - dto.ElPresentFlag = entity.ElPresentFlag; - dto.BlPresentFlag = entity.BlPresentFlag; - dto.DvBlSignalCompatibilityId = entity.DvBlSignalCompatibilityId; - dto.IsHearingImpaired = entity.IsHearingImpaired; - dto.Rotation = entity.Rotation; - - if (dto.Type is MediaStreamType.Audio or MediaStreamType.Subtitle) - { - dto.LocalizedDefault = localization.GetLocalizedString("Default"); - dto.LocalizedExternal = localization.GetLocalizedString("External"); - - if (dto.Type is MediaStreamType.Subtitle) - { - dto.LocalizedUndefined = localization.GetLocalizedString("Undefined"); - dto.LocalizedForced = localization.GetLocalizedString("Forced"); - dto.LocalizedHearingImpaired = localization.GetLocalizedString("HearingImpaired"); - } - } - - return dto; - } - - private MediaStreamInfo Map(MediaStream dto, Guid itemId) - { - var entity = new MediaStreamInfo - { - Item = null!, - ItemId = itemId, - StreamIndex = dto.Index, - StreamType = dto.Type.ToString(), - IsAvc = dto.IsAVC.GetValueOrDefault(), - - Codec = dto.Codec, - Language = dto.Language, - ChannelLayout = dto.ChannelLayout, - Profile = dto.Profile, - AspectRatio = dto.AspectRatio, - Path = GetPathToSave(dto.Path), - IsInterlaced = dto.IsInterlaced, - BitRate = dto.BitRate.GetValueOrDefault(0), - Channels = dto.Channels.GetValueOrDefault(0), - SampleRate = dto.SampleRate.GetValueOrDefault(0), - IsDefault = dto.IsDefault, - IsForced = dto.IsForced, - IsExternal = dto.IsExternal, - Height = dto.Height.GetValueOrDefault(0), - Width = dto.Width.GetValueOrDefault(0), - AverageFrameRate = dto.AverageFrameRate.GetValueOrDefault(0), - RealFrameRate = dto.RealFrameRate.GetValueOrDefault(0), - Level = (float)dto.Level.GetValueOrDefault(), - PixelFormat = dto.PixelFormat, - BitDepth = dto.BitDepth.GetValueOrDefault(0), - IsAnamorphic = dto.IsAnamorphic.GetValueOrDefault(0), - RefFrames = dto.RefFrames.GetValueOrDefault(0), - CodecTag = dto.CodecTag, - Comment = dto.Comment, - NalLengthSize = dto.NalLengthSize, - Title = dto.Title, - TimeBase = dto.TimeBase, - CodecTimeBase = dto.CodecTimeBase, - ColorPrimaries = dto.ColorPrimaries, - ColorSpace = dto.ColorSpace, - ColorTransfer = dto.ColorTransfer, - DvVersionMajor = dto.DvVersionMajor.GetValueOrDefault(0), - DvVersionMinor = dto.DvVersionMinor.GetValueOrDefault(0), - DvProfile = dto.DvProfile.GetValueOrDefault(0), - DvLevel = dto.DvLevel.GetValueOrDefault(0), - RpuPresentFlag = dto.RpuPresentFlag.GetValueOrDefault(0), - ElPresentFlag = dto.ElPresentFlag.GetValueOrDefault(0), - BlPresentFlag = dto.BlPresentFlag.GetValueOrDefault(0), - DvBlSignalCompatibilityId = dto.DvBlSignalCompatibilityId.GetValueOrDefault(0), - IsHearingImpaired = dto.IsHearingImpaired, - Rotation = dto.Rotation.GetValueOrDefault(0) - }; - return entity; - } -} diff --git a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs new file mode 100644 index 0000000000..f7b714c296 --- /dev/null +++ b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; +using Microsoft.EntityFrameworkCore; + +namespace Jellyfin.Server.Implementations.Item; + +/// +/// Initializes a new instance of the class. +/// +/// The EFCore db factory. +/// The Application host. +/// The Localisation Provider. +public class MediaStreamRepository(IDbContextFactory dbProvider, IServerApplicationHost serverApplicationHost, ILocalizationManager localization) : IMediaStreamRepository +{ + /// + public void SaveMediaStreams(Guid id, IReadOnlyList streams, CancellationToken cancellationToken) + { + using var context = dbProvider.CreateDbContext(); + using var transaction = context.Database.BeginTransaction(); + + context.MediaStreamInfos.Where(e => e.ItemId.Equals(id)).ExecuteDelete(); + context.MediaStreamInfos.AddRange(streams.Select(f => Map(f, id))); + context.SaveChanges(); + + transaction.Commit(); + } + + /// + public IReadOnlyList GetMediaStreams(MediaStreamQuery filter) + { + using var context = dbProvider.CreateDbContext(); + return TranslateQuery(context.MediaStreamInfos, filter).ToList().Select(Map).ToImmutableArray(); + } + + private string? GetPathToSave(string? path) + { + if (path is null) + { + return null; + } + + return serverApplicationHost.ReverseVirtualPath(path); + } + + private string? RestorePath(string? path) + { + if (path is null) + { + return null; + } + + return serverApplicationHost.ExpandVirtualPath(path); + } + + private IQueryable TranslateQuery(IQueryable query, MediaStreamQuery filter) + { + query = query.Where(e => e.ItemId.Equals(filter.ItemId)); + if (filter.Index.HasValue) + { + query = query.Where(e => e.StreamIndex == filter.Index); + } + + if (filter.Type.HasValue) + { + query = query.Where(e => e.StreamType == filter.Type.ToString()); + } + + return query; + } + + private MediaStream Map(MediaStreamInfo entity) + { + var dto = new MediaStream(); + dto.Index = entity.StreamIndex; + if (entity.StreamType != null) + { + dto.Type = Enum.Parse(entity.StreamType); + } + + dto.IsAVC = entity.IsAvc; + dto.Codec = entity.Codec; + dto.Language = entity.Language; + dto.ChannelLayout = entity.ChannelLayout; + dto.Profile = entity.Profile; + dto.AspectRatio = entity.AspectRatio; + dto.Path = RestorePath(entity.Path); + dto.IsInterlaced = entity.IsInterlaced; + dto.BitRate = entity.BitRate; + dto.Channels = entity.Channels; + dto.SampleRate = entity.SampleRate; + dto.IsDefault = entity.IsDefault; + dto.IsForced = entity.IsForced; + dto.IsExternal = entity.IsExternal; + dto.Height = entity.Height; + dto.Width = entity.Width; + dto.AverageFrameRate = entity.AverageFrameRate; + dto.RealFrameRate = entity.RealFrameRate; + dto.Level = entity.Level; + dto.PixelFormat = entity.PixelFormat; + dto.BitDepth = entity.BitDepth; + dto.IsAnamorphic = entity.IsAnamorphic; + dto.RefFrames = entity.RefFrames; + dto.CodecTag = entity.CodecTag; + dto.Comment = entity.Comment; + dto.NalLengthSize = entity.NalLengthSize; + dto.Title = entity.Title; + dto.TimeBase = entity.TimeBase; + dto.CodecTimeBase = entity.CodecTimeBase; + dto.ColorPrimaries = entity.ColorPrimaries; + dto.ColorSpace = entity.ColorSpace; + dto.ColorTransfer = entity.ColorTransfer; + dto.DvVersionMajor = entity.DvVersionMajor; + dto.DvVersionMinor = entity.DvVersionMinor; + dto.DvProfile = entity.DvProfile; + dto.DvLevel = entity.DvLevel; + dto.RpuPresentFlag = entity.RpuPresentFlag; + dto.ElPresentFlag = entity.ElPresentFlag; + dto.BlPresentFlag = entity.BlPresentFlag; + dto.DvBlSignalCompatibilityId = entity.DvBlSignalCompatibilityId; + dto.IsHearingImpaired = entity.IsHearingImpaired; + dto.Rotation = entity.Rotation; + + if (dto.Type is MediaStreamType.Audio or MediaStreamType.Subtitle) + { + dto.LocalizedDefault = localization.GetLocalizedString("Default"); + dto.LocalizedExternal = localization.GetLocalizedString("External"); + + if (dto.Type is MediaStreamType.Subtitle) + { + dto.LocalizedUndefined = localization.GetLocalizedString("Undefined"); + dto.LocalizedForced = localization.GetLocalizedString("Forced"); + dto.LocalizedHearingImpaired = localization.GetLocalizedString("HearingImpaired"); + } + } + + return dto; + } + + private MediaStreamInfo Map(MediaStream dto, Guid itemId) + { + var entity = new MediaStreamInfo + { + Item = null!, + ItemId = itemId, + StreamIndex = dto.Index, + StreamType = dto.Type.ToString(), + IsAvc = dto.IsAVC.GetValueOrDefault(), + + Codec = dto.Codec, + Language = dto.Language, + ChannelLayout = dto.ChannelLayout, + Profile = dto.Profile, + AspectRatio = dto.AspectRatio, + Path = GetPathToSave(dto.Path), + IsInterlaced = dto.IsInterlaced, + BitRate = dto.BitRate.GetValueOrDefault(0), + Channels = dto.Channels.GetValueOrDefault(0), + SampleRate = dto.SampleRate.GetValueOrDefault(0), + IsDefault = dto.IsDefault, + IsForced = dto.IsForced, + IsExternal = dto.IsExternal, + Height = dto.Height.GetValueOrDefault(0), + Width = dto.Width.GetValueOrDefault(0), + AverageFrameRate = dto.AverageFrameRate.GetValueOrDefault(0), + RealFrameRate = dto.RealFrameRate.GetValueOrDefault(0), + Level = (float)dto.Level.GetValueOrDefault(), + PixelFormat = dto.PixelFormat, + BitDepth = dto.BitDepth.GetValueOrDefault(0), + IsAnamorphic = dto.IsAnamorphic.GetValueOrDefault(0), + RefFrames = dto.RefFrames.GetValueOrDefault(0), + CodecTag = dto.CodecTag, + Comment = dto.Comment, + NalLengthSize = dto.NalLengthSize, + Title = dto.Title, + TimeBase = dto.TimeBase, + CodecTimeBase = dto.CodecTimeBase, + ColorPrimaries = dto.ColorPrimaries, + ColorSpace = dto.ColorSpace, + ColorTransfer = dto.ColorTransfer, + DvVersionMajor = dto.DvVersionMajor.GetValueOrDefault(0), + DvVersionMinor = dto.DvVersionMinor.GetValueOrDefault(0), + DvProfile = dto.DvProfile.GetValueOrDefault(0), + DvLevel = dto.DvLevel.GetValueOrDefault(0), + RpuPresentFlag = dto.RpuPresentFlag.GetValueOrDefault(0), + ElPresentFlag = dto.ElPresentFlag.GetValueOrDefault(0), + BlPresentFlag = dto.BlPresentFlag.GetValueOrDefault(0), + DvBlSignalCompatibilityId = dto.DvBlSignalCompatibilityId.GetValueOrDefault(0), + IsHearingImpaired = dto.IsHearingImpaired, + Rotation = dto.Rotation.GetValueOrDefault(0) + }; + return entity; + } +} diff --git a/Jellyfin.Server.Implementations/Item/PeopleManager.cs b/Jellyfin.Server.Implementations/Item/PeopleManager.cs deleted file mode 100644 index d29d8b143e..0000000000 --- a/Jellyfin.Server.Implementations/Item/PeopleManager.cs +++ /dev/null @@ -1,164 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; -using Jellyfin.Extensions; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Persistence; -using Microsoft.EntityFrameworkCore; - -namespace Jellyfin.Server.Implementations.Item; - -/// -/// Manager for handling people. -/// -/// Efcore Factory. -/// -/// Initializes a new instance of the class. -/// -/// The EFCore Context factory. -public class PeopleManager(IDbContextFactory dbProvider) : IPeopleManager -{ - private readonly IDbContextFactory _dbProvider = dbProvider; - - public IReadOnlyList GetPeople(InternalPeopleQuery filter) - { - using var context = _dbProvider.CreateDbContext(); - var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter); - - dbQuery = dbQuery.OrderBy(e => e.ListOrder); - if (filter.Limit > 0) - { - dbQuery = dbQuery.Take(filter.Limit); - } - - return dbQuery.ToList().Select(Map).ToImmutableArray(); - } - - public IReadOnlyList GetPeopleNames(InternalPeopleQuery filter) - { - using var context = _dbProvider.CreateDbContext(); - var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter); - - dbQuery = dbQuery.OrderBy(e => e.ListOrder); - if (filter.Limit > 0) - { - dbQuery = dbQuery.Take(filter.Limit); - } - - return dbQuery.Select(e => e.Name).ToImmutableArray(); - } - - /// - public void UpdatePeople(Guid itemId, IReadOnlyList people) - { - using var context = _dbProvider.CreateDbContext(); - using var transaction = context.Database.BeginTransaction(); - - context.Peoples.Where(e => e.ItemId.Equals(itemId)).ExecuteDelete(); - context.Peoples.AddRange(people.Select(Map)); - context.SaveChanges(); - transaction.Commit(); - } - - private PersonInfo Map(People people) - { - var personInfo = new PersonInfo() - { - ItemId = people.ItemId, - Name = people.Name, - Role = people.Role, - SortOrder = people.SortOrder, - }; - if (Enum.TryParse(people.PersonType, out var kind)) - { - personInfo.Type = kind; - } - - return personInfo; - } - - private People Map(PersonInfo people) - { - var personInfo = new People() - { - ItemId = people.ItemId, - Name = people.Name, - Role = people.Role, - SortOrder = people.SortOrder, - PersonType = people.Type.ToString() - }; - - return personInfo; - } - - private IQueryable TranslateQuery(IQueryable query, JellyfinDbContext context, InternalPeopleQuery filter) - { - if (filter.User is not null && filter.IsFavorite.HasValue) - { - query = query.Where(e => e.PersonType == typeof(Person).FullName) - .Where(e => context.BaseItems.Where(d => context.UserData.Where(e => e.IsFavorite == filter.IsFavorite && e.UserId.Equals(filter.User.Id)).Any(f => f.Key == d.UserDataKey)) - .Select(f => f.Name).Contains(e.Name)); - } - - if (!filter.ItemId.IsEmpty()) - { - query = query.Where(e => e.ItemId.Equals(filter.ItemId)); - } - - if (!filter.AppearsInItemId.IsEmpty()) - { - query = query.Where(e => context.Peoples.Where(f => f.ItemId.Equals(filter.AppearsInItemId)).Select(e => e.Name).Contains(e.Name)); - } - - var queryPersonTypes = filter.PersonTypes.Where(IsValidPersonType).ToList(); - if (queryPersonTypes.Count > 0) - { - query = query.Where(e => queryPersonTypes.Contains(e.PersonType)); - } - - var queryExcludePersonTypes = filter.ExcludePersonTypes.Where(IsValidPersonType).ToList(); - - if (queryExcludePersonTypes.Count > 0) - { - query = query.Where(e => !queryPersonTypes.Contains(e.PersonType)); - } - - if (filter.MaxListOrder.HasValue) - { - query = query.Where(e => e.ListOrder <= filter.MaxListOrder.Value); - } - - if (!string.IsNullOrWhiteSpace(filter.NameContains)) - { - query = query.Where(e => e.Name.Contains(filter.NameContains)); - } - - return query; - } - - private bool IsAlphaNumeric(string str) - { - if (string.IsNullOrWhiteSpace(str)) - { - return false; - } - - for (int i = 0; i < str.Length; i++) - { - if (!char.IsLetter(str[i]) && !char.IsNumber(str[i])) - { - return false; - } - } - - return true; - } - - private bool IsValidPersonType(string value) - { - return IsAlphaNumeric(value); - } -} diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs new file mode 100644 index 0000000000..3ced6e24e3 --- /dev/null +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using Jellyfin.Extensions; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace Jellyfin.Server.Implementations.Item; + +/// +/// Manager for handling people. +/// +/// Efcore Factory. +/// +/// Initializes a new instance of the class. +/// +public class PeopleRepository(IDbContextFactory dbProvider) : IPeopleRepository +{ + private readonly IDbContextFactory _dbProvider = dbProvider; + + /// + public IReadOnlyList GetPeople(InternalPeopleQuery filter) + { + using var context = _dbProvider.CreateDbContext(); + var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter); + + dbQuery = dbQuery.OrderBy(e => e.ListOrder); + if (filter.Limit > 0) + { + dbQuery = dbQuery.Take(filter.Limit); + } + + return dbQuery.ToList().Select(Map).ToImmutableArray(); + } + + /// + public IReadOnlyList GetPeopleNames(InternalPeopleQuery filter) + { + using var context = _dbProvider.CreateDbContext(); + var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter); + + dbQuery = dbQuery.OrderBy(e => e.ListOrder); + if (filter.Limit > 0) + { + dbQuery = dbQuery.Take(filter.Limit); + } + + return dbQuery.Select(e => e.Name).ToImmutableArray(); + } + + /// + public void UpdatePeople(Guid itemId, IReadOnlyList people) + { + using var context = _dbProvider.CreateDbContext(); + using var transaction = context.Database.BeginTransaction(); + + context.Peoples.Where(e => e.ItemId.Equals(itemId)).ExecuteDelete(); + context.Peoples.AddRange(people.Select(Map)); + context.SaveChanges(); + transaction.Commit(); + } + + private PersonInfo Map(People people) + { + var personInfo = new PersonInfo() + { + ItemId = people.ItemId, + Name = people.Name, + Role = people.Role, + SortOrder = people.SortOrder, + }; + if (Enum.TryParse(people.PersonType, out var kind)) + { + personInfo.Type = kind; + } + + return personInfo; + } + + private People Map(PersonInfo people) + { + var personInfo = new People() + { + ItemId = people.ItemId, + Name = people.Name, + Role = people.Role, + SortOrder = people.SortOrder, + PersonType = people.Type.ToString() + }; + + return personInfo; + } + + private IQueryable TranslateQuery(IQueryable query, JellyfinDbContext context, InternalPeopleQuery filter) + { + if (filter.User is not null && filter.IsFavorite.HasValue) + { + query = query.Where(e => e.PersonType == typeof(Person).FullName) + .Where(e => context.BaseItems.Where(d => context.UserData.Where(e => e.IsFavorite == filter.IsFavorite && e.UserId.Equals(filter.User.Id)).Any(f => f.Key == d.UserDataKey)) + .Select(f => f.Name).Contains(e.Name)); + } + + if (!filter.ItemId.IsEmpty()) + { + query = query.Where(e => e.ItemId.Equals(filter.ItemId)); + } + + if (!filter.AppearsInItemId.IsEmpty()) + { + query = query.Where(e => context.Peoples.Where(f => f.ItemId.Equals(filter.AppearsInItemId)).Select(e => e.Name).Contains(e.Name)); + } + + var queryPersonTypes = filter.PersonTypes.Where(IsValidPersonType).ToList(); + if (queryPersonTypes.Count > 0) + { + query = query.Where(e => queryPersonTypes.Contains(e.PersonType)); + } + + var queryExcludePersonTypes = filter.ExcludePersonTypes.Where(IsValidPersonType).ToList(); + + if (queryExcludePersonTypes.Count > 0) + { + query = query.Where(e => !queryPersonTypes.Contains(e.PersonType)); + } + + if (filter.MaxListOrder.HasValue) + { + query = query.Where(e => e.ListOrder <= filter.MaxListOrder.Value); + } + + if (!string.IsNullOrWhiteSpace(filter.NameContains)) + { + query = query.Where(e => e.Name.Contains(filter.NameContains)); + } + + return query; + } + + private bool IsAlphaNumeric(string str) + { + if (string.IsNullOrWhiteSpace(str)) + { + return false; + } + + for (int i = 0; i < str.Length; i++) + { + if (!char.IsLetter(str[i]) && !char.IsNumber(str[i])) + { + return false; + } + } + + return true; + } + + private bool IsValidPersonType(string value) + { + return IsAlphaNumeric(value); + } +} diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs index fcc20a0d4f..c1d6d58cdf 100644 --- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs +++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs @@ -106,7 +106,7 @@ public class JellyfinDbContext : DbContext /// /// Gets the containing the user data. /// - public DbSet BaseItems => Set(); + public DbSet BaseItems => Set(); /// /// Gets the containing the user data. diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs index c0f09670d7..4aba9d07e1 100644 --- a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs +++ b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs @@ -8,10 +8,10 @@ namespace Jellyfin.Server.Implementations.ModelConfiguration; /// /// Configuration for BaseItem. /// -public class BaseItemConfiguration : IEntityTypeConfiguration +public class BaseItemConfiguration : IEntityTypeConfiguration { /// - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { builder.HasNoKey(); builder.HasIndex(e => e.Path); diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs index f34837c57c..d15049a1fa 100644 --- a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs +++ b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs @@ -13,7 +13,7 @@ public class BaseItemProviderConfiguration : IEntityTypeConfiguration public void Configure(EntityTypeBuilder builder) { - builder.HasNoKey(); + builder.HasKey(e => new { e.ItemId, e.ProviderId }); builder.HasOne(e => e.Item); builder.HasIndex(e => new { e.ProviderId, e.ProviderValue, e.ItemId }); } diff --git a/MediaBrowser.Controller/Chapters/ChapterManager.cs b/MediaBrowser.Controller/Chapters/ChapterManager.cs deleted file mode 100644 index a9e11f603a..0000000000 --- a/MediaBrowser.Controller/Chapters/ChapterManager.cs +++ /dev/null @@ -1,24 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using MediaBrowser.Controller.Chapters; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Providers.Chapters -{ - public class ChapterManager : IChapterManager - { - public ChapterManager(IDbContextFactory dbProvider) - { - _itemRepo = itemRepo; - } - - /// - public void SaveChapters(Guid itemId, IReadOnlyList chapters) - { - _itemRepo.SaveChapters(itemId, chapters); - } - } -} diff --git a/MediaBrowser.Controller/Chapters/IChapterManager.cs b/MediaBrowser.Controller/Chapters/IChapterManager.cs deleted file mode 100644 index 55762c7fc4..0000000000 --- a/MediaBrowser.Controller/Chapters/IChapterManager.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Controller.Chapters -{ - /// - /// Interface IChapterManager. - /// - public interface IChapterManager - { - /// - /// Saves the chapters. - /// - /// The item. - /// The set of chapters. - void SaveChapters(Guid itemId, IReadOnlyList chapters); - - /// - /// Gets all chapters associated with the baseItem. - /// - /// The baseitem. - /// A readonly list of chapter instances. - IReadOnlyList GetChapters(BaseItemDto baseItem); - - /// - /// Gets a single chapter of a BaseItem on a specific index. - /// - /// The baseitem. - /// The index of that chapter. - /// A chapter instance. - ChapterInfo? GetChapter(BaseItemDto baseItem, int index); - } -} diff --git a/MediaBrowser.Controller/Chapters/IChapterRepository.cs b/MediaBrowser.Controller/Chapters/IChapterRepository.cs new file mode 100644 index 0000000000..e22cb0f584 --- /dev/null +++ b/MediaBrowser.Controller/Chapters/IChapterRepository.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Chapters; + +/// +/// Interface IChapterManager. +/// +public interface IChapterRepository +{ + /// + /// Saves the chapters. + /// + /// The item. + /// The set of chapters. + void SaveChapters(Guid itemId, IReadOnlyList chapters); + + /// + /// Gets all chapters associated with the baseItem. + /// + /// The baseitem. + /// A readonly list of chapter instances. + IReadOnlyList GetChapters(BaseItemDto baseItem); + + /// + /// Gets a single chapter of a BaseItem on a specific index. + /// + /// The baseitem. + /// The index of that chapter. + /// A chapter instance. + ChapterInfo? GetChapter(BaseItemDto baseItem, int index); + + /// + /// Gets all chapters associated with the baseItem. + /// + /// The BaseItems id. + /// A readonly list of chapter instances. + IReadOnlyList GetChapters(Guid baseItemId); + + /// + /// Gets a single chapter of a BaseItem on a specific index. + /// + /// The BaseItems id. + /// The index of that chapter. + /// A chapter instance. + ChapterInfo? GetChapter(Guid baseItemId, int index); +} diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index 0d1e2a5a07..702ce39a2a 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Drawing @@ -57,6 +58,22 @@ namespace MediaBrowser.Controller.Drawing /// BlurHash. string GetImageBlurHash(string path, ImageDimensions imageDimensions); + /// + /// Gets the image cache tag. + /// + /// The items basePath. + /// The image last modification date. + /// Guid. + string? GetImageCacheTag(string baseItemPath, DateTime imageDateModified); + + /// + /// Gets the image cache tag. + /// + /// The item. + /// The image. + /// Guid. + string? GetImageCacheTag(BaseItemDto item, ChapterInfo image); + /// /// Gets the image cache tag. /// @@ -65,6 +82,14 @@ namespace MediaBrowser.Controller.Drawing /// Guid. string GetImageCacheTag(BaseItem item, ItemImageInfo image); + /// + /// Gets the image cache tag. + /// + /// The item. + /// The image. + /// Guid. + string GetImageCacheTag(BaseItemDto item, ItemImageInfo image); + string? GetImageCacheTag(BaseItem item, ChapterInfo chapter); string? GetImageCacheTag(User user); diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index eb605f6c87..a4764dd33f 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -16,6 +16,7 @@ using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities.Audio; @@ -479,6 +480,8 @@ namespace MediaBrowser.Controller.Entities public static IItemRepository ItemRepository { get; set; } + public static IChapterRepository ChapterRepository { get; set; } + public static IFileSystem FileSystem { get; set; } public static IUserDataManager UserDataManager { get; set; } @@ -2031,7 +2034,7 @@ namespace MediaBrowser.Controller.Entities { if (imageType == ImageType.Chapter) { - var chapter = ItemRepository.GetChapter(this, imageIndex); + var chapter = ChapterRepository.GetChapter(this.Id, imageIndex); if (chapter is null) { @@ -2081,7 +2084,7 @@ namespace MediaBrowser.Controller.Entities if (image.Type == ImageType.Chapter) { - var chapters = ItemRepository.GetChapters(this); + var chapters = ChapterRepository.GetChapters(this.Id); for (var i = 0; i < chapters.Count; i++) { if (chapters[i].ImagePath == image.Path) diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 313b1459ab..b27f156efe 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -52,7 +52,6 @@ public interface IItemRepository : IDisposable /// List<Guid>. IReadOnlyList GetItemIdsList(InternalItemsQuery filter); - /// /// Gets the item list. /// diff --git a/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs b/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs new file mode 100644 index 0000000000..1b2ca2acb5 --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using Jellyfin.Data.Enums; +using MediaBrowser.Model.Querying; + +namespace MediaBrowser.Controller.Persistence; + +/// +/// Provides static lookup data for and for the domain. +/// +public interface IItemTypeLookup +{ + /// + /// Gets all values of the ItemFields type. + /// + public IReadOnlyList AllItemFields { get; } + + /// + /// Gets all BaseItemKinds that are considered Programs. + /// + public IReadOnlyList ProgramTypes { get; } + + /// + /// Gets all BaseItemKinds that should be excluded from parent lookup. + /// + public IReadOnlyList ProgramExcludeParentTypes { get; } + + /// + /// Gets all BaseItemKinds that are considered to be provided by services. + /// + public IReadOnlyList ServiceTypes { get; } + + /// + /// Gets all BaseItemKinds that have a StartDate. + /// + public IReadOnlyList StartDateTypes { get; } + + /// + /// Gets all BaseItemKinds that are considered Series. + /// + public IReadOnlyList SeriesTypes { get; } + + /// + /// Gets all BaseItemKinds that are not to be evaluated for Artists. + /// + public IReadOnlyList ArtistExcludeParentTypes { get; } + + /// + /// Gets all BaseItemKinds that are considered Artists. + /// + public IReadOnlyList ArtistsTypes { get; } + + /// + /// Gets mapping for all BaseItemKinds and their expected serialisaition target. + /// + public IDictionary BaseItemKindNames { get; } +} diff --git a/MediaBrowser.Controller/Persistence/IMediaAttachmentManager.cs b/MediaBrowser.Controller/Persistence/IMediaAttachmentManager.cs deleted file mode 100644 index 210d80afa2..0000000000 --- a/MediaBrowser.Controller/Persistence/IMediaAttachmentManager.cs +++ /dev/null @@ -1,29 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Threading; -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Controller.Persistence; - -public interface IMediaAttachmentManager -{ - - /// - /// Gets the media attachments. - /// - /// The query. - /// IEnumerable{MediaAttachment}. - IReadOnlyList GetMediaAttachments(MediaAttachmentQuery filter); - - /// - /// Saves the media attachments. - /// - /// The identifier. - /// The attachments. - /// The cancellation token. - void SaveMediaAttachments(Guid id, IReadOnlyList attachments, CancellationToken cancellationToken); -} diff --git a/MediaBrowser.Controller/Persistence/IMediaAttachmentRepository.cs b/MediaBrowser.Controller/Persistence/IMediaAttachmentRepository.cs new file mode 100644 index 0000000000..4773f40581 --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IMediaAttachmentRepository.cs @@ -0,0 +1,28 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Threading; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Persistence; + +public interface IMediaAttachmentRepository +{ + /// + /// Gets the media attachments. + /// + /// The query. + /// IEnumerable{MediaAttachment}. + IReadOnlyList GetMediaAttachments(MediaAttachmentQuery filter); + + /// + /// Saves the media attachments. + /// + /// The identifier. + /// The attachments. + /// The cancellation token. + void SaveMediaAttachments(Guid id, IReadOnlyList attachments, CancellationToken cancellationToken); +} diff --git a/MediaBrowser.Controller/Persistence/IMediaStreamManager.cs b/MediaBrowser.Controller/Persistence/IMediaStreamManager.cs deleted file mode 100644 index ec7c72935b..0000000000 --- a/MediaBrowser.Controller/Persistence/IMediaStreamManager.cs +++ /dev/null @@ -1,28 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Threading; -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Controller.Persistence; - -public interface IMediaStreamManager -{ - /// - /// Gets the media streams. - /// - /// The query. - /// IEnumerable{MediaStream}. - List GetMediaStreams(MediaStreamQuery filter); - - /// - /// Saves the media streams. - /// - /// The identifier. - /// The streams. - /// The cancellation token. - void SaveMediaStreams(Guid id, IReadOnlyList streams, CancellationToken cancellationToken); -} diff --git a/MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs b/MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs new file mode 100644 index 0000000000..665129eafd --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs @@ -0,0 +1,31 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Threading; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Persistence; + +/// +/// Provides methods for accessing MediaStreams. +/// +public interface IMediaStreamRepository +{ + /// + /// Gets the media streams. + /// + /// The query. + /// IEnumerable{MediaStream}. + IReadOnlyList GetMediaStreams(MediaStreamQuery filter); + + /// + /// Saves the media streams. + /// + /// The identifier. + /// The streams. + /// The cancellation token. + void SaveMediaStreams(Guid id, IReadOnlyList streams, CancellationToken cancellationToken); +} diff --git a/MediaBrowser.Controller/Persistence/IPeopleManager.cs b/MediaBrowser.Controller/Persistence/IPeopleManager.cs deleted file mode 100644 index 84e503fefb..0000000000 --- a/MediaBrowser.Controller/Persistence/IPeopleManager.cs +++ /dev/null @@ -1,34 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using MediaBrowser.Controller.Entities; - -namespace MediaBrowser.Controller.Persistence; - -public interface IPeopleManager -{ - /// - /// Gets the people. - /// - /// The query. - /// List<PersonInfo>. - IReadOnlyList GetPeople(InternalPeopleQuery filter); - - /// - /// Updates the people. - /// - /// The item identifier. - /// The people. - void UpdatePeople(Guid itemId, IReadOnlyList people); - - /// - /// Gets the people names. - /// - /// The query. - /// List<System.String>. - IReadOnlyList GetPeopleNames(InternalPeopleQuery filter); - -} diff --git a/MediaBrowser.Controller/Persistence/IPeopleRepository.cs b/MediaBrowser.Controller/Persistence/IPeopleRepository.cs new file mode 100644 index 0000000000..43a24703e4 --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IPeopleRepository.cs @@ -0,0 +1,33 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Persistence; + +public interface IPeopleRepository +{ + /// + /// Gets the people. + /// + /// The query. + /// List<PersonInfo>. + IReadOnlyList GetPeople(InternalPeopleQuery filter); + + /// + /// Updates the people. + /// + /// The item identifier. + /// The people. + void UpdatePeople(Guid itemId, IReadOnlyList people); + + /// + /// Gets the people names. + /// + /// The query. + /// List<System.String>. + IReadOnlyList GetPeopleNames(InternalPeopleQuery filter); +} diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 246ba2733f..62c5909441 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -38,7 +38,7 @@ namespace MediaBrowser.Providers.MediaInfo private readonly IEncodingManager _encodingManager; private readonly IServerConfigurationManager _config; private readonly ISubtitleManager _subtitleManager; - private readonly IChapterManager _chapterManager; + private readonly IChapterRepository _chapterManager; private readonly ILibraryManager _libraryManager; private readonly AudioResolver _audioResolver; private readonly SubtitleResolver _subtitleResolver; @@ -54,7 +54,7 @@ namespace MediaBrowser.Providers.MediaInfo IEncodingManager encodingManager, IServerConfigurationManager config, ISubtitleManager subtitleManager, - IChapterManager chapterManager, + IChapterRepository chapterManager, ILibraryManager libraryManager, AudioResolver audioResolver, SubtitleResolver subtitleResolver) diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs index 04da8fb882..f5e9dddcfc 100644 --- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs @@ -61,7 +61,7 @@ namespace MediaBrowser.Providers.MediaInfo /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. + /// Instance of the interface. /// Instance of the interface. /// Instance of the . /// Instance of the interface. @@ -76,7 +76,7 @@ namespace MediaBrowser.Providers.MediaInfo IEncodingManager encodingManager, IServerConfigurationManager config, ISubtitleManager subtitleManager, - IChapterManager chapterManager, + IChapterRepository chapterManager, ILibraryManager libraryManager, IFileSystem fileSystem, ILoggerFactory loggerFactory, diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs index 5d4732234d..b57f2753f3 100644 --- a/src/Jellyfin.Drawing/ImageProcessor.cs +++ b/src/Jellyfin.Drawing/ImageProcessor.cs @@ -15,6 +15,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; @@ -403,10 +404,34 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable return _imageEncoder.GetImageBlurHash(xComp, yComp, path); } + /// + public string GetImageCacheTag(string baseItemPath, DateTime imageDateModified) + => (baseItemPath + imageDateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture); + /// public string GetImageCacheTag(BaseItem item, ItemImageInfo image) => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture); + /// + public string GetImageCacheTag(BaseItemDto item, ItemImageInfo image) + => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture); + + /// + public string? GetImageCacheTag(BaseItemDto item, ChapterInfo chapter) + { + if (chapter.ImagePath is null) + { + return null; + } + + return GetImageCacheTag(item, new ItemImageInfo + { + Path = chapter.ImagePath, + Type = ImageType.Chapter, + DateModified = chapter.ImageDateModified + }); + } + /// public string? GetImageCacheTag(BaseItem item, ChapterInfo chapter) { -- cgit v1.2.3 From b09a41ad1f05664a6099734cb44e068f993a8e93 Mon Sep 17 00:00:00 2001 From: JPVenson <6794763+JPVenson@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:36:08 +0000 Subject: WIP porting new Repository structure --- .editorconfig | 3 ++ Emby.Server.Implementations/Dto/DtoService.cs | 12 +++++--- .../Library/LibraryManager.cs | 35 ++++++++++++---------- .../Library/MediaSourceManager.cs | 25 +++++++++------- .../Library/MusicManager.cs | 19 ++++++------ .../Library/SearchEngine.cs | 2 +- .../ScheduledTasks/Tasks/ChapterImagesTask.cs | 9 ++++-- Jellyfin.Api/Controllers/LibraryController.cs | 2 +- Jellyfin.Api/Controllers/MoviesController.cs | 4 +-- Jellyfin.Api/Controllers/YearsController.cs | 7 +++-- .../Item/MediaStreamRepository.cs | 2 +- .../Item/PeopleRepository.cs | 4 ++- .../ModelConfiguration/ChapterConfiguration.cs | 1 - .../Trickplay/TrickplayManager.cs | 2 +- .../Entities/AggregateFolder.cs | 2 +- .../Entities/Audio/MusicArtist.cs | 2 +- .../Entities/Audio/MusicGenre.cs | 2 +- MediaBrowser.Controller/Entities/BaseItem.cs | 9 +++--- MediaBrowser.Controller/Entities/Folder.cs | 31 ++++++++++--------- MediaBrowser.Controller/Entities/Genre.cs | 2 +- .../Entities/IHasMediaSources.cs | 4 +-- MediaBrowser.Controller/Entities/IItemByName.cs | 2 +- MediaBrowser.Controller/Entities/Movies/BoxSet.cs | 13 ++++---- MediaBrowser.Controller/Entities/PeopleHelper.cs | 2 +- MediaBrowser.Controller/Entities/Person.cs | 2 +- MediaBrowser.Controller/Entities/Studio.cs | 2 +- MediaBrowser.Controller/Entities/TV/Series.cs | 4 +-- MediaBrowser.Controller/Entities/UserRootFolder.cs | 2 +- MediaBrowser.Controller/Entities/UserView.cs | 4 +-- .../Entities/UserViewBuilder.cs | 2 +- MediaBrowser.Controller/Entities/Year.cs | 2 +- MediaBrowser.Controller/Library/ILibraryManager.cs | 18 +++++------ .../Library/IMediaSourceManager.cs | 8 ++--- MediaBrowser.Controller/Library/IMusicManager.cs | 6 ++-- MediaBrowser.Controller/LiveTv/LiveTvChannel.cs | 9 +++--- MediaBrowser.Controller/Playlists/Playlist.cs | 10 +++---- .../Providers/MetadataResult.cs | 16 ++++++---- .../BoxSets/BoxSetMetadataService.cs | 2 +- MediaBrowser.Providers/Manager/MetadataService.cs | 25 +++++++--------- .../MediaInfo/AudioFileProber.cs | 8 +++-- .../MediaInfo/AudioImageProvider.cs | 2 +- .../MediaInfo/FFProbeVideoInfo.cs | 15 +++++++--- MediaBrowser.Providers/MediaInfo/ProbeProvider.cs | 13 ++++++-- .../MediaInfo/SubtitleDownloader.cs | 6 ++-- .../Music/AlbumMetadataService.cs | 4 +-- .../Music/ArtistMetadataService.cs | 5 ++-- .../Playlists/PlaylistMetadataService.cs | 2 +- MediaBrowser.Providers/TV/SeasonMetadataService.cs | 6 ++-- MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs | 2 +- MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs | 2 +- 50 files changed, 211 insertions(+), 162 deletions(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/.editorconfig b/.editorconfig index b84e563efa..147b76c141 100644 --- a/.editorconfig +++ b/.editorconfig @@ -192,3 +192,6 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false # Wrapping preferences csharp_preserve_single_line_statements = true csharp_preserve_single_line_blocks = true + +# CA1826: Do not use Enumerable methods on indexable collections +dotnet_diagnostic.CA1826.severity = suggestion diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 0c0ba74533..356d1e437a 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -10,6 +10,7 @@ using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common; using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -51,6 +52,7 @@ namespace Emby.Server.Implementations.Dto private readonly Lazy _livetvManagerFactory; private readonly ITrickplayManager _trickplayManager; + private readonly IChapterRepository _chapterRepository; public DtoService( ILogger logger, @@ -63,7 +65,8 @@ namespace Emby.Server.Implementations.Dto IApplicationHost appHost, IMediaSourceManager mediaSourceManager, Lazy livetvManagerFactory, - ITrickplayManager trickplayManager) + ITrickplayManager trickplayManager, + IChapterRepository chapterRepository) { _logger = logger; _libraryManager = libraryManager; @@ -76,6 +79,7 @@ namespace Emby.Server.Implementations.Dto _mediaSourceManager = mediaSourceManager; _livetvManagerFactory = livetvManagerFactory; _trickplayManager = trickplayManager; + _chapterRepository = chapterRepository; } private ILiveTvManager LivetvManager => _livetvManagerFactory.Value; @@ -165,7 +169,7 @@ namespace Emby.Server.Implementations.Dto return dto; } - private static IList GetTaggedItems(IItemByName byName, User? user, DtoOptions options) + private static IReadOnlyList GetTaggedItems(IItemByName byName, User? user, DtoOptions options) { return byName.GetTaggedItems( new InternalItemsQuery(user) @@ -327,7 +331,7 @@ namespace Emby.Server.Implementations.Dto return dto; } - private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IList taggedItems) + private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IReadOnlyList taggedItems) { if (item is MusicArtist) { @@ -1060,7 +1064,7 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.Chapters)) { - dto.Chapters = _itemRepo.GetChapters(item); + dto.Chapters = _chapterRepository.GetChapters(item.Id).ToList(); } if (options.ContainsField(ItemFields.Trickplay)) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 28f7ed6598..0a98d54351 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -76,6 +76,7 @@ namespace Emby.Server.Implementations.Library private readonly IItemRepository _itemRepository; private readonly IImageProcessor _imageProcessor; private readonly NamingOptions _namingOptions; + private readonly IPeopleRepository _peopleRepository; private readonly ExtraResolver _extraResolver; /// @@ -112,6 +113,7 @@ namespace Emby.Server.Implementations.Library /// The image processor. /// The naming options. /// The directory service. + /// The People Repository. public LibraryManager( IServerApplicationHost appHost, ILoggerFactory loggerFactory, @@ -127,7 +129,8 @@ namespace Emby.Server.Implementations.Library IItemRepository itemRepository, IImageProcessor imageProcessor, NamingOptions namingOptions, - IDirectoryService directoryService) + IDirectoryService directoryService, + IPeopleRepository peopleRepository) { _appHost = appHost; _logger = loggerFactory.CreateLogger(); @@ -144,7 +147,7 @@ namespace Emby.Server.Implementations.Library _imageProcessor = imageProcessor; _cache = new ConcurrentDictionary(); _namingOptions = namingOptions; - + _peopleRepository = peopleRepository; _extraResolver = new ExtraResolver(loggerFactory.CreateLogger(), namingOptions, directoryService); _configurationManager.ConfigurationUpdated += ConfigurationUpdated; @@ -1274,7 +1277,7 @@ namespace Emby.Server.Implementations.Library return ItemIsVisible(item, user) ? item : null; } - public List GetItemList(InternalItemsQuery query, bool allowExternalContent) + public IReadOnlyList GetItemList(InternalItemsQuery query, bool allowExternalContent) { if (query.Recursive && !query.ParentId.IsEmpty()) { @@ -1300,7 +1303,7 @@ namespace Emby.Server.Implementations.Library return itemList; } - public List GetItemList(InternalItemsQuery query) + public IReadOnlyList GetItemList(InternalItemsQuery query) { return GetItemList(query, true); } @@ -1324,7 +1327,7 @@ namespace Emby.Server.Implementations.Library return _itemRepository.GetCount(query); } - public List GetItemList(InternalItemsQuery query, List parents) + public IReadOnlyList GetItemList(InternalItemsQuery query, List parents) { SetTopParentIdsOrAncestors(query, parents); @@ -1357,7 +1360,7 @@ namespace Emby.Server.Implementations.Library _itemRepository.GetItemList(query)); } - public List GetItemIds(InternalItemsQuery query) + public IReadOnlyList GetItemIds(InternalItemsQuery query) { if (query.User is not null) { @@ -2736,12 +2739,12 @@ namespace Emby.Server.Implementations.Library return path; } - public List GetPeople(InternalPeopleQuery query) + public IReadOnlyList GetPeople(InternalPeopleQuery query) { - return _itemRepository.GetPeople(query); + return _peopleRepository.GetPeople(query); } - public List GetPeople(BaseItem item) + public IReadOnlyList GetPeople(BaseItem item) { if (item.SupportsPeople) { @@ -2756,12 +2759,12 @@ namespace Emby.Server.Implementations.Library } } - return new List(); + return []; } - public List GetPeopleItems(InternalPeopleQuery query) + public IReadOnlyList GetPeopleItems(InternalPeopleQuery query) { - return _itemRepository.GetPeopleNames(query) + return _peopleRepository.GetPeopleNames(query) .Select(i => { try @@ -2779,9 +2782,9 @@ namespace Emby.Server.Implementations.Library .ToList()!; // null values are filtered out } - public List GetPeopleNames(InternalPeopleQuery query) + public IReadOnlyList GetPeopleNames(InternalPeopleQuery query) { - return _itemRepository.GetPeopleNames(query); + return _peopleRepository.GetPeopleNames(query); } public void UpdatePeople(BaseItem item, List people) @@ -2790,14 +2793,14 @@ namespace Emby.Server.Implementations.Library } /// - public async Task UpdatePeopleAsync(BaseItem item, List people, CancellationToken cancellationToken) + public async Task UpdatePeopleAsync(BaseItem item, IReadOnlyList people, CancellationToken cancellationToken) { if (!item.SupportsPeople) { return; } - _itemRepository.UpdatePeople(item.Id, people); + _peopleRepository.UpdatePeople(item.Id, people); if (people is not null) { await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 90a01c052c..a5a715721f 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -51,7 +51,8 @@ namespace Emby.Server.Implementations.Library private readonly ILocalizationManager _localizationManager; private readonly IApplicationPaths _appPaths; private readonly IDirectoryService _directoryService; - + private readonly IMediaStreamRepository _mediaStreamRepository; + private readonly IMediaAttachmentRepository _mediaAttachmentRepository; private readonly ConcurrentDictionary _openStreams = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); private readonly AsyncNonKeyedLocker _liveStreamLocker = new(1); private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; @@ -69,7 +70,9 @@ namespace Emby.Server.Implementations.Library IFileSystem fileSystem, IUserDataManager userDataManager, IMediaEncoder mediaEncoder, - IDirectoryService directoryService) + IDirectoryService directoryService, + IMediaStreamRepository mediaStreamRepository, + IMediaAttachmentRepository mediaAttachmentRepository) { _appHost = appHost; _itemRepo = itemRepo; @@ -82,6 +85,8 @@ namespace Emby.Server.Implementations.Library _localizationManager = localizationManager; _appPaths = applicationPaths; _directoryService = directoryService; + _mediaStreamRepository = mediaStreamRepository; + _mediaAttachmentRepository = mediaAttachmentRepository; } public void AddParts(IEnumerable providers) @@ -89,9 +94,9 @@ namespace Emby.Server.Implementations.Library _providers = providers.ToArray(); } - public List GetMediaStreams(MediaStreamQuery query) + public IReadOnlyList GetMediaStreams(MediaStreamQuery query) { - var list = _itemRepo.GetMediaStreams(query); + var list = _mediaStreamRepository.GetMediaStreams(query); foreach (var stream in list) { @@ -121,7 +126,7 @@ namespace Emby.Server.Implementations.Library return false; } - public List GetMediaStreams(Guid itemId) + public IReadOnlyList GetMediaStreams(Guid itemId) { var list = GetMediaStreams(new MediaStreamQuery { @@ -131,7 +136,7 @@ namespace Emby.Server.Implementations.Library return GetMediaStreamsForItem(list); } - private List GetMediaStreamsForItem(List streams) + private IReadOnlyList GetMediaStreamsForItem(IReadOnlyList streams) { foreach (var stream in streams) { @@ -145,13 +150,13 @@ namespace Emby.Server.Implementations.Library } /// - public List GetMediaAttachments(MediaAttachmentQuery query) + public IReadOnlyList GetMediaAttachments(MediaAttachmentQuery query) { - return _itemRepo.GetMediaAttachments(query); + return _mediaAttachmentRepository.GetMediaAttachments(query); } /// - public List GetMediaAttachments(Guid itemId) + public IReadOnlyList GetMediaAttachments(Guid itemId) { return GetMediaAttachments(new MediaAttachmentQuery { @@ -332,7 +337,7 @@ namespace Emby.Server.Implementations.Library return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase)); } - public List GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null) + public IReadOnlyList GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null) { ArgumentNullException.ThrowIfNull(item); diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index a69a0f33f3..c83737cec2 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; @@ -24,7 +25,7 @@ namespace Emby.Server.Implementations.Library _libraryManager = libraryManager; } - public List GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions) + public IReadOnlyList GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions) { var list = new List { @@ -33,21 +34,21 @@ namespace Emby.Server.Implementations.Library list.AddRange(GetInstantMixFromGenres(item.Genres, user, dtoOptions)); - return list; + return list.ToImmutableList(); } /// - public List GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions) + public IReadOnlyList GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions) { return GetInstantMixFromGenres(artist.Genres, user, dtoOptions); } - public List GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions) + public IReadOnlyList GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions) { return GetInstantMixFromGenres(item.Genres, user, dtoOptions); } - public List GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions) + public IReadOnlyList GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions) { var genres = item .GetRecursiveChildren(user, new InternalItemsQuery(user) @@ -63,12 +64,12 @@ namespace Emby.Server.Implementations.Library return GetInstantMixFromGenres(genres, user, dtoOptions); } - public List GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions) + public IReadOnlyList GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions) { return GetInstantMixFromGenres(item.Genres, user, dtoOptions); } - public List GetInstantMixFromGenres(IEnumerable genres, User? user, DtoOptions dtoOptions) + public IReadOnlyList GetInstantMixFromGenres(IEnumerable genres, User? user, DtoOptions dtoOptions) { var genreIds = genres.DistinctNames().Select(i => { @@ -85,7 +86,7 @@ namespace Emby.Server.Implementations.Library return GetInstantMixFromGenreIds(genreIds, user, dtoOptions); } - public List GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions) + public IReadOnlyList GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions) { return _libraryManager.GetItemList(new InternalItemsQuery(user) { @@ -97,7 +98,7 @@ namespace Emby.Server.Implementations.Library }); } - public List GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions) + public IReadOnlyList GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions) { if (item is MusicGenre) { diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index 7f3f8615e2..3ac1d02192 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -171,7 +171,7 @@ namespace Emby.Server.Implementations.Library } }; - List mediaItems; + IReadOnlyList mediaItems; if (searchQuery.IncludeItemTypes.Length == 1 && searchQuery.IncludeItemTypes[0] == BaseItemKind.MusicArtist) { diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index cb3f5b8363..c0ab535a34 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -32,6 +33,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks private readonly IEncodingManager _encodingManager; private readonly IFileSystem _fileSystem; private readonly ILocalizationManager _localization; + private readonly IChapterRepository _chapterRepository; /// /// Initializes a new instance of the class. @@ -43,6 +45,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. + /// Instance of the interface. public ChapterImagesTask( ILogger logger, ILibraryManager libraryManager, @@ -50,7 +53,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks IApplicationPaths appPaths, IEncodingManager encodingManager, IFileSystem fileSystem, - ILocalizationManager localization) + ILocalizationManager localization, + IChapterRepository chapterRepository) { _logger = logger; _libraryManager = libraryManager; @@ -59,6 +63,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks _encodingManager = encodingManager; _fileSystem = fileSystem; _localization = localization; + _chapterRepository = chapterRepository; } /// @@ -141,7 +146,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks try { - var chapters = _itemRepo.GetChapters(video); + var chapters = _chapterRepository.GetChapters(video.Id); var success = await _encodingManager.RefreshChapterImages(video, directoryService, chapters, extract, true, cancellationToken).ConfigureAwait(false); diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index afc93c3a8d..b2d75d5a38 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -793,7 +793,7 @@ public class LibraryController : BaseJellyfinApiController query.ExcludeArtistIds = excludeArtistIds; } - List itemsResult = _libraryManager.GetItemList(query); + var itemsResult = _libraryManager.GetItemList(query); var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user); diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index 471bcd096e..11559419c1 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -97,7 +97,7 @@ public class MoviesController : BaseJellyfinApiController DtoOptions = dtoOptions }; - var recentlyPlayedMovies = _libraryManager.GetItemList(query); + var recentlyPlayedMovies = _libraryManager.GetItemList(query)!; var itemTypes = new List { BaseItemKind.Movie }; if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) @@ -120,7 +120,7 @@ public class MoviesController : BaseJellyfinApiController DtoOptions = dtoOptions }); - var mostRecentMovies = recentlyPlayedMovies.GetRange(0, Math.Min(recentlyPlayedMovies.Count, 6)); + var mostRecentMovies = recentlyPlayedMovies.Take(Math.Min(recentlyPlayedMovies.Count, 6)); // Get recently played directors var recentDirectors = GetDirectors(mostRecentMovies) .ToList(); diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index e4aa0ea42d..ffc34a5d97 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; using System.Linq; using Jellyfin.Api.Extensions; @@ -105,18 +106,18 @@ public class YearsController : BaseJellyfinApiController bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes); - IList items; + IReadOnlyList items; if (parentItem.IsFolder) { var folder = (Folder)parentItem; if (userId.IsNullOrEmpty()) { - items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToList(); + items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToImmutableList(); } else { - items = recursive ? folder.GetRecursiveChildren(user, query).ToList() : folder.GetChildren(user, true).Where(Filter).ToList(); + items = recursive ? folder.GetRecursiveChildren(user, query) : folder.GetChildren(user, true).Where(Filter).ToImmutableList(); } } else diff --git a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs index f7b714c296..f44ead6e02 100644 --- a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs +++ b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs @@ -174,7 +174,7 @@ public class MediaStreamRepository(IDbContextFactory dbProvid Level = (float)dto.Level.GetValueOrDefault(), PixelFormat = dto.PixelFormat, BitDepth = dto.BitDepth.GetValueOrDefault(0), - IsAnamorphic = dto.IsAnamorphic.GetValueOrDefault(0), + IsAnamorphic = dto.IsAnamorphic.GetValueOrDefault(), RefFrames = dto.RefFrames.GetValueOrDefault(0), CodecTag = dto.CodecTag, Comment = dto.Comment, diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index 3ced6e24e3..584dbd1b65 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -89,7 +89,9 @@ public class PeopleRepository(IDbContextFactory dbProvider) : Name = people.Name, Role = people.Role, SortOrder = people.SortOrder, - PersonType = people.Type.ToString() + PersonType = people.Type.ToString(), + Item = null!, + ListOrder = people.SortOrder }; return personInfo; diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/ChapterConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/ChapterConfiguration.cs index 0e7c88931a..464fbfb014 100644 --- a/Jellyfin.Server.Implementations/ModelConfiguration/ChapterConfiguration.cs +++ b/Jellyfin.Server.Implementations/ModelConfiguration/ChapterConfiguration.cs @@ -5,7 +5,6 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace Jellyfin.Server.Implementations.ModelConfiguration; - /// /// Chapter configuration. /// diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index f6c48498ca..9fe3ee010b 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -179,7 +179,7 @@ public class TrickplayManager : ITrickplayManager { // Extract images // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay. - var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id)); + var mediaSource = video.GetMediaSources(false).FirstOrDefault(source => Guid.Parse(source.Id).Equals(video.Id)); if (mediaSource is null) { diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs index 40cdd6c91e..00b06dc79c 100644 --- a/MediaBrowser.Controller/Entities/AggregateFolder.cs +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -64,7 +64,7 @@ namespace MediaBrowser.Controller.Entities return CreateResolveArgs(directoryService, true).FileSystemChildren; } - protected override List LoadChildren() + protected override IReadOnlyList LoadChildren() { lock (_childIdsLock) { diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 1ab6c97066..6d3249399b 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -84,7 +84,7 @@ namespace MediaBrowser.Controller.Entities.Audio return !IsAccessedByName; } - public IList GetTaggedItems(InternalItemsQuery query) + public IReadOnlyList GetTaggedItems(InternalItemsQuery query) { if (query.IncludeItemTypes.Length == 0) { diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs index 7448d02ea5..80f3902be7 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs @@ -64,7 +64,7 @@ namespace MediaBrowser.Controller.Entities.Audio return true; } - public IList GetTaggedItems(InternalItemsQuery query) + public IReadOnlyList GetTaggedItems(InternalItemsQuery query) { query.GenreIds = new[] { Id }; query.IncludeItemTypes = new[] { BaseItemKind.MusicVideo, BaseItemKind.Audio, BaseItemKind.MusicAlbum, BaseItemKind.MusicArtist }; diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index a4764dd33f..054c71db7e 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.IO; using System.Linq; @@ -1044,7 +1045,7 @@ namespace MediaBrowser.Controller.Entities return PlayAccess.Full; } - public virtual List GetMediaStreams() + public virtual IReadOnlyList GetMediaStreams() { return MediaSourceManager.GetMediaStreams(new MediaStreamQuery { @@ -1057,7 +1058,7 @@ namespace MediaBrowser.Controller.Entities return false; } - public virtual List GetMediaSources(bool enablePathSubstitution) + public virtual IReadOnlyList GetMediaSources(bool enablePathSubstitution) { if (SourceType == SourceType.Channel) { @@ -1091,7 +1092,7 @@ namespace MediaBrowser.Controller.Entities return 1; }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0) .ThenByDescending(i => i, new MediaSourceWidthComparator()) - .ToList(); + .ToImmutableList(); } protected virtual IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources() @@ -2527,7 +2528,7 @@ namespace MediaBrowser.Controller.Entities /// /// Media children. /// true if the rating was updated; otherwise false. - public bool UpdateRatingToItems(IList children) + public bool UpdateRatingToItems(IReadOnlyList children) { var currentOfficialRating = OfficialRating; diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 83c19a54e1..1bec66f952 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; using System.Linq; using System.Security; @@ -11,6 +12,7 @@ using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; +using J2N.Collections.Generic.Extensions; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; @@ -247,7 +249,7 @@ namespace MediaBrowser.Controller.Entities /// We want this synchronous. /// /// Returns children. - protected virtual List LoadChildren() + protected virtual IReadOnlyList LoadChildren() { // logger.LogDebug("Loading children from {0} {1} {2}", GetType().Name, Id, Path); // just load our children from the repo - the library will be validated and maintained in other processes @@ -659,7 +661,7 @@ namespace MediaBrowser.Controller.Entities /// Get our children from the repo - stubbed for now. /// /// IEnumerable{BaseItem}. - protected List GetCachedChildren() + protected IReadOnlyList GetCachedChildren() { return ItemRepository.GetItemList(new InternalItemsQuery { @@ -1283,14 +1285,14 @@ namespace MediaBrowser.Controller.Entities return true; } - public List GetChildren(User user, bool includeLinkedChildren) + public IReadOnlyList GetChildren(User user, bool includeLinkedChildren) { ArgumentNullException.ThrowIfNull(user); return GetChildren(user, includeLinkedChildren, new InternalItemsQuery(user)); } - public virtual List GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + public virtual IReadOnlyList GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) { ArgumentNullException.ThrowIfNull(user); @@ -1304,7 +1306,7 @@ namespace MediaBrowser.Controller.Entities AddChildren(user, includeLinkedChildren, result, false, query); - return result.Values.ToList(); + return result.Values.ToImmutableList(); } protected virtual IEnumerable GetEligibleChildrenForRecursiveChildren(User user) @@ -1369,7 +1371,7 @@ namespace MediaBrowser.Controller.Entities } } - public virtual IEnumerable GetRecursiveChildren(User user, InternalItemsQuery query) + public virtual IReadOnlyList GetRecursiveChildren(User user, InternalItemsQuery query) { ArgumentNullException.ThrowIfNull(user); @@ -1377,35 +1379,35 @@ namespace MediaBrowser.Controller.Entities AddChildren(user, true, result, true, query); - return result.Values; + return result.Values.ToImmutableList(); } /// /// Gets the recursive children. /// /// IList{BaseItem}. - public IList GetRecursiveChildren() + public IReadOnlyList GetRecursiveChildren() { return GetRecursiveChildren(true); } - public IList GetRecursiveChildren(bool includeLinkedChildren) + public IReadOnlyList GetRecursiveChildren(bool includeLinkedChildren) { return GetRecursiveChildren(i => true, includeLinkedChildren); } - public IList GetRecursiveChildren(Func filter) + public IReadOnlyList GetRecursiveChildren(Func filter) { return GetRecursiveChildren(filter, true); } - public IList GetRecursiveChildren(Func filter, bool includeLinkedChildren) + public IReadOnlyList GetRecursiveChildren(Func filter, bool includeLinkedChildren) { var result = new Dictionary(); AddChildrenToList(result, includeLinkedChildren, true, filter); - return result.Values.ToList(); + return result.Values.ToImmutableList(); } /// @@ -1556,11 +1558,12 @@ namespace MediaBrowser.Controller.Entities /// Gets the linked children. /// /// IEnumerable{BaseItem}. - public IEnumerable> GetLinkedChildrenInfos() + public IReadOnlyList> GetLinkedChildrenInfos() { return LinkedChildren .Select(i => new Tuple(i, GetLinkedChild(i))) - .Where(i => i.Item2 is not null); + .Where(i => i.Item2 is not null) + .ToImmutableList(); } protected override async Task RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList fileSystemChildren, CancellationToken cancellationToken) diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs index ddf62dd4cb..e5353d7bd9 100644 --- a/MediaBrowser.Controller/Entities/Genre.cs +++ b/MediaBrowser.Controller/Entities/Genre.cs @@ -61,7 +61,7 @@ namespace MediaBrowser.Controller.Entities return false; } - public IList GetTaggedItems(InternalItemsQuery query) + public IReadOnlyList GetTaggedItems(InternalItemsQuery query) { query.GenreIds = new[] { Id }; query.ExcludeItemTypes = new[] diff --git a/MediaBrowser.Controller/Entities/IHasMediaSources.cs b/MediaBrowser.Controller/Entities/IHasMediaSources.cs index 90d9bdd2d3..ad35494c28 100644 --- a/MediaBrowser.Controller/Entities/IHasMediaSources.cs +++ b/MediaBrowser.Controller/Entities/IHasMediaSources.cs @@ -22,8 +22,8 @@ namespace MediaBrowser.Controller.Entities /// /// true to enable path substitution, false to not. /// A list of media sources. - List GetMediaSources(bool enablePathSubstitution); + IReadOnlyList GetMediaSources(bool enablePathSubstitution); - List GetMediaStreams(); + IReadOnlyList GetMediaStreams(); } } diff --git a/MediaBrowser.Controller/Entities/IItemByName.cs b/MediaBrowser.Controller/Entities/IItemByName.cs index cac8aa61a5..4928bda7a2 100644 --- a/MediaBrowser.Controller/Entities/IItemByName.cs +++ b/MediaBrowser.Controller/Entities/IItemByName.cs @@ -9,7 +9,7 @@ namespace MediaBrowser.Controller.Entities /// public interface IItemByName { - IList GetTaggedItems(InternalItemsQuery query); + IReadOnlyList GetTaggedItems(InternalItemsQuery query); } public interface IHasDualAccess : IItemByName diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index a07187d2fd..4cddc91252 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Text.Json.Serialization; using Jellyfin.Data.Entities; @@ -91,7 +92,7 @@ namespace MediaBrowser.Controller.Entities.Movies return Enumerable.Empty(); } - protected override List LoadChildren() + protected override IReadOnlyList LoadChildren() { if (IsLegacyBoxSet) { @@ -99,7 +100,7 @@ namespace MediaBrowser.Controller.Entities.Movies } // Save a trip to the database - return new List(); + return []; } public override bool IsAuthorizedToDelete(User user, List allCollectionFolders) @@ -127,16 +128,16 @@ namespace MediaBrowser.Controller.Entities.Movies return LibraryManager.Sort(items, user, new[] { sortBy }, SortOrder.Ascending); } - public override List GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + public override IReadOnlyList GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) { var children = base.GetChildren(user, includeLinkedChildren, query); - return Sort(children, user).ToList(); + return Sort(children, user).ToImmutableList(); } - public override IEnumerable GetRecursiveChildren(User user, InternalItemsQuery query) + public override IReadOnlyList GetRecursiveChildren(User user, InternalItemsQuery query) { var children = base.GetRecursiveChildren(user, query); - return Sort(children, user).ToList(); + return Sort(children, user).ToImmutableList(); } public BoxSetInfo GetLookupInfo() diff --git a/MediaBrowser.Controller/Entities/PeopleHelper.cs b/MediaBrowser.Controller/Entities/PeopleHelper.cs index 5292bd7727..4141b17127 100644 --- a/MediaBrowser.Controller/Entities/PeopleHelper.cs +++ b/MediaBrowser.Controller/Entities/PeopleHelper.cs @@ -10,7 +10,7 @@ namespace MediaBrowser.Controller.Entities { public static class PeopleHelper { - public static void AddPerson(List people, PersonInfo person) + public static void AddPerson(ICollection people, PersonInfo person) { ArgumentNullException.ThrowIfNull(person); ArgumentException.ThrowIfNullOrEmpty(person.Name); diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs index 7f265084fb..b0933d23f4 100644 --- a/MediaBrowser.Controller/Entities/Person.cs +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -62,7 +62,7 @@ namespace MediaBrowser.Controller.Entities return value; } - public IList GetTaggedItems(InternalItemsQuery query) + public IReadOnlyList GetTaggedItems(InternalItemsQuery query) { query.PersonIds = new[] { Id }; diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs index a3736a4bfc..b46a3d1bcf 100644 --- a/MediaBrowser.Controller/Entities/Studio.cs +++ b/MediaBrowser.Controller/Entities/Studio.cs @@ -63,7 +63,7 @@ namespace MediaBrowser.Controller.Entities return true; } - public IList GetTaggedItems(InternalItemsQuery query) + public IReadOnlyList GetTaggedItems(InternalItemsQuery query) { query.StudioIds = new[] { Id }; diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index a324f79eff..137d91f1cf 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -189,12 +189,12 @@ namespace MediaBrowser.Controller.Entities.TV return list; } - public override List GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + public override IReadOnlyList GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) { return GetSeasons(user, new DtoOptions(true)); } - public List GetSeasons(User user, DtoOptions options) + public IReadOnlyList GetSeasons(User user, DtoOptions options) { var query = new InternalItemsQuery(user) { diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs index a687adeddc..7cf447fb8d 100644 --- a/MediaBrowser.Controller/Entities/UserRootFolder.cs +++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs @@ -52,7 +52,7 @@ namespace MediaBrowser.Controller.Entities } } - protected override List LoadChildren() + protected override IReadOnlyList LoadChildren() { lock (_childIdsLock) { diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index e4fb340f78..f5ca3737c2 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -134,7 +134,7 @@ namespace MediaBrowser.Controller.Entities } /// - public override IEnumerable GetRecursiveChildren(User user, InternalItemsQuery query) + public override IReadOnlyList GetRecursiveChildren(User user, InternalItemsQuery query) { query.SetUser(user); query.Recursive = true; @@ -145,7 +145,7 @@ namespace MediaBrowser.Controller.Entities } /// - protected override IEnumerable GetEligibleChildrenForRecursiveChildren(User user) + protected override IReadOnlyList GetEligibleChildrenForRecursiveChildren(User user) { return GetChildren(user, false); } diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 420349f35c..4ec2e4c0a4 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -236,7 +236,7 @@ namespace MediaBrowser.Controller.Entities return ConvertToResult(_libraryManager.GetItemList(query)); } - private QueryResult ConvertToResult(List items) + private QueryResult ConvertToResult(IReadOnlyList items) { return new QueryResult(items); } diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs index afdaf448b7..587d7ce7e5 100644 --- a/MediaBrowser.Controller/Entities/Year.cs +++ b/MediaBrowser.Controller/Entities/Year.cs @@ -55,7 +55,7 @@ namespace MediaBrowser.Controller.Entities return true; } - public IList GetTaggedItems(InternalItemsQuery query) + public IReadOnlyList GetTaggedItems(InternalItemsQuery query) { if (!int.TryParse(Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year)) { diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index b802b7e6ea..47b1cb16e8 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -483,21 +483,21 @@ namespace MediaBrowser.Controller.Library /// /// The item. /// List<PersonInfo>. - List GetPeople(BaseItem item); + IReadOnlyList GetPeople(BaseItem item); /// /// Gets the people. /// /// The query. /// List<PersonInfo>. - List GetPeople(InternalPeopleQuery query); + IReadOnlyList GetPeople(InternalPeopleQuery query); /// /// Gets the people items. /// /// The query. /// List<Person>. - List GetPeopleItems(InternalPeopleQuery query); + IReadOnlyList GetPeopleItems(InternalPeopleQuery query); /// /// Updates the people. @@ -513,21 +513,21 @@ namespace MediaBrowser.Controller.Library /// The people. /// The cancellation token. /// The async task. - Task UpdatePeopleAsync(BaseItem item, List people, CancellationToken cancellationToken); + Task UpdatePeopleAsync(BaseItem item, IReadOnlyList people, CancellationToken cancellationToken); /// /// Gets the item ids. /// /// The query. /// List<Guid>. - List GetItemIds(InternalItemsQuery query); + IReadOnlyList GetItemIds(InternalItemsQuery query); /// /// Gets the people names. /// /// The query. /// List<System.String>. - List GetPeopleNames(InternalPeopleQuery query); + IReadOnlyList GetPeopleNames(InternalPeopleQuery query); /// /// Queries the items. @@ -553,9 +553,9 @@ namespace MediaBrowser.Controller.Library /// /// The query. /// QueryResult<BaseItem>. - List GetItemList(InternalItemsQuery query); + IReadOnlyList GetItemList(InternalItemsQuery query); - List GetItemList(InternalItemsQuery query, bool allowExternalContent); + IReadOnlyList GetItemList(InternalItemsQuery query, bool allowExternalContent); /// /// Gets the items. @@ -563,7 +563,7 @@ namespace MediaBrowser.Controller.Library /// The query to use. /// Items to use for query. /// List of items. - List GetItemList(InternalItemsQuery query, List parents); + IReadOnlyList GetItemList(InternalItemsQuery query, List parents); /// /// Gets the items result. diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs index 44a1a85e30..5ed3a49c38 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs @@ -29,28 +29,28 @@ namespace MediaBrowser.Controller.Library /// /// The item identifier. /// IEnumerable<MediaStream>. - List GetMediaStreams(Guid itemId); + IReadOnlyList GetMediaStreams(Guid itemId); /// /// Gets the media streams. /// /// The query. /// IEnumerable<MediaStream>. - List GetMediaStreams(MediaStreamQuery query); + IReadOnlyList GetMediaStreams(MediaStreamQuery query); /// /// Gets the media attachments. /// /// The item identifier. /// IEnumerable<MediaAttachment>. - List GetMediaAttachments(Guid itemId); + IReadOnlyList GetMediaAttachments(Guid itemId); /// /// Gets the media attachments. /// /// The query. /// IEnumerable<MediaAttachment>. - List GetMediaAttachments(MediaAttachmentQuery query); + IReadOnlyList GetMediaAttachments(MediaAttachmentQuery query); /// /// Gets the playack media sources. diff --git a/MediaBrowser.Controller/Library/IMusicManager.cs b/MediaBrowser.Controller/Library/IMusicManager.cs index 93073cc79b..7ba8fc20cf 100644 --- a/MediaBrowser.Controller/Library/IMusicManager.cs +++ b/MediaBrowser.Controller/Library/IMusicManager.cs @@ -17,7 +17,7 @@ namespace MediaBrowser.Controller.Library /// The user to use. /// The options to use. /// List of items. - List GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions); + IReadOnlyList GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions); /// /// Gets the instant mix from artist. @@ -26,7 +26,7 @@ namespace MediaBrowser.Controller.Library /// The user to use. /// The options to use. /// List of items. - List GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions); + IReadOnlyList GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions); /// /// Gets the instant mix from genre. @@ -35,6 +35,6 @@ namespace MediaBrowser.Controller.Library /// The user to use. /// The options to use. /// List of items. - List GetInstantMixFromGenres(IEnumerable genres, User? user, DtoOptions dtoOptions); + IReadOnlyList GetInstantMixFromGenres(IEnumerable genres, User? user, DtoOptions dtoOptions); } } diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs index 3c2cf8e3d2..64d49d8c48 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Text.Json.Serialization; @@ -122,7 +123,7 @@ namespace MediaBrowser.Controller.LiveTv public IEnumerable GetTaggedItems() => Enumerable.Empty(); - public override List GetMediaSources(bool enablePathSubstitution) + public override IReadOnlyList GetMediaSources(bool enablePathSubstitution) { var list = new List(); @@ -140,12 +141,12 @@ namespace MediaBrowser.Controller.LiveTv list.Add(info); - return list; + return list.ToImmutableList(); } - public override List GetMediaStreams() + public override IReadOnlyList GetMediaStreams() { - return new List(); + return []; } protected override string GetInternalMetadataPath(string basePath) diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index 45aefacf6d..bf6871a745 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -137,27 +137,27 @@ namespace MediaBrowser.Controller.Playlists return Task.CompletedTask; } - public override List GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + public override IReadOnlyList GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) { return GetPlayableItems(user, query); } - protected override IEnumerable GetNonCachedChildren(IDirectoryService directoryService) + protected override IReadOnlyList GetNonCachedChildren(IDirectoryService directoryService) { return []; } - public override IEnumerable GetRecursiveChildren(User user, InternalItemsQuery query) + public override IReadOnlyList GetRecursiveChildren(User user, InternalItemsQuery query) { return GetPlayableItems(user, query); } - public IEnumerable> GetManageableItems() + public IReadOnlyList> GetManageableItems() { return GetLinkedChildrenInfos(); } - private List GetPlayableItems(User user, InternalItemsQuery query) + private IReadOnlyList GetPlayableItems(User user, InternalItemsQuery query) { query ??= new InternalItemsQuery(user); diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs index cfff3eb144..eabbe73cde 100644 --- a/MediaBrowser.Controller/Providers/MetadataResult.cs +++ b/MediaBrowser.Controller/Providers/MetadataResult.cs @@ -3,6 +3,7 @@ #pragma warning disable CA1002, CA2227, CS1591 using System.Collections.Generic; +using System.Linq; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Entities; @@ -13,6 +14,7 @@ namespace MediaBrowser.Controller.Providers // Images aren't always used so the allocation is a waste a lot of the time private List _images; private List<(string Url, ImageType Type)> _remoteImages; + private List _people; public MetadataResult() { @@ -21,17 +23,21 @@ namespace MediaBrowser.Controller.Providers public List Images { - get => _images ??= new List(); + get => _images ??= []; set => _images = value; } public List<(string Url, ImageType Type)> RemoteImages { - get => _remoteImages ??= new List<(string Url, ImageType Type)>(); + get => _remoteImages ??= []; set => _remoteImages = value; } - public List People { get; set; } + public IReadOnlyList People + { + get => _people; + set => _people = value.ToList(); + } public bool HasMetadata { get; set; } @@ -47,7 +53,7 @@ namespace MediaBrowser.Controller.Providers { People ??= new List(); - PeopleHelper.AddPerson(People, p); + PeopleHelper.AddPerson(_people, p); } /// @@ -61,7 +67,7 @@ namespace MediaBrowser.Controller.Providers } else { - People.Clear(); + _people.Clear(); } } } diff --git a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs index 32ab7716f7..b51ab4c08e 100644 --- a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs +++ b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs @@ -39,7 +39,7 @@ namespace MediaBrowser.Providers.BoxSets protected override bool EnableUpdatingPremiereDateFromChildren => true; /// - protected override IList GetChildrenForMetadataUpdates(BoxSet item) + protected override IReadOnlyList GetChildrenForMetadataUpdates(BoxSet item) { return item.GetLinkedChildren(); } diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 7203bf1158..4c9d162c4b 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -322,17 +322,17 @@ namespace MediaBrowser.Providers.Manager return false; } - protected virtual IList GetChildrenForMetadataUpdates(TItemType item) + protected virtual IReadOnlyList GetChildrenForMetadataUpdates(TItemType item) { if (item is Folder folder) { return folder.GetRecursiveChildren(); } - return Array.Empty(); + return []; } - protected virtual ItemUpdateType UpdateMetadataFromChildren(TItemType item, IList children, bool isFullRefresh, ItemUpdateType currentUpdateType) + protected virtual ItemUpdateType UpdateMetadataFromChildren(TItemType item, IReadOnlyList children, bool isFullRefresh, ItemUpdateType currentUpdateType) { var updateType = ItemUpdateType.None; @@ -371,7 +371,7 @@ namespace MediaBrowser.Providers.Manager return updateType; } - private ItemUpdateType UpdateCumulativeRunTimeTicks(TItemType item, IList children) + private ItemUpdateType UpdateCumulativeRunTimeTicks(TItemType item, IReadOnlyList children) { if (item is Folder folder && folder.SupportsCumulativeRunTimeTicks) { @@ -395,7 +395,7 @@ namespace MediaBrowser.Providers.Manager return ItemUpdateType.None; } - private ItemUpdateType UpdateDateLastMediaAdded(TItemType item, IList children) + private ItemUpdateType UpdateDateLastMediaAdded(TItemType item, IReadOnlyList children) { var updateType = ItemUpdateType.None; @@ -429,7 +429,7 @@ namespace MediaBrowser.Providers.Manager return updateType; } - private ItemUpdateType UpdatePremiereDate(TItemType item, IList children) + private ItemUpdateType UpdatePremiereDate(TItemType item, IReadOnlyList children) { var updateType = ItemUpdateType.None; @@ -467,7 +467,7 @@ namespace MediaBrowser.Providers.Manager return updateType; } - private ItemUpdateType UpdateGenres(TItemType item, IList children) + private ItemUpdateType UpdateGenres(TItemType item, IReadOnlyList children) { var updateType = ItemUpdateType.None; @@ -488,7 +488,7 @@ namespace MediaBrowser.Providers.Manager return updateType; } - private ItemUpdateType UpdateStudios(TItemType item, IList children) + private ItemUpdateType UpdateStudios(TItemType item, IReadOnlyList children) { var updateType = ItemUpdateType.None; @@ -509,7 +509,7 @@ namespace MediaBrowser.Providers.Manager return updateType; } - private ItemUpdateType UpdateOfficialRating(TItemType item, IList children) + private ItemUpdateType UpdateOfficialRating(TItemType item, IReadOnlyList children) { var updateType = ItemUpdateType.None; @@ -1142,13 +1142,8 @@ namespace MediaBrowser.Providers.Manager } } - private static void MergePeople(List source, List target) + private static void MergePeople(IReadOnlyList source, IReadOnlyList target) { - if (target is null) - { - target = new List(); - } - foreach (var person in target) { var normalizedName = person.Name.RemoveDiacritics(); diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 27f6d120f9..3add439f9c 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -36,6 +36,7 @@ namespace MediaBrowser.Providers.MediaInfo private readonly IMediaSourceManager _mediaSourceManager; private readonly LyricResolver _lyricResolver; private readonly ILyricManager _lyricManager; + private readonly IMediaStreamRepository _mediaStreamRepository; /// /// Initializes a new instance of the class. @@ -47,6 +48,7 @@ namespace MediaBrowser.Providers.MediaInfo /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. + /// Instance of the . public AudioFileProber( ILogger logger, IMediaSourceManager mediaSourceManager, @@ -54,7 +56,8 @@ namespace MediaBrowser.Providers.MediaInfo IItemRepository itemRepo, ILibraryManager libraryManager, LyricResolver lyricResolver, - ILyricManager lyricManager) + ILyricManager lyricManager, + IMediaStreamRepository mediaStreamRepository) { _mediaEncoder = mediaEncoder; _itemRepo = itemRepo; @@ -63,6 +66,7 @@ namespace MediaBrowser.Providers.MediaInfo _mediaSourceManager = mediaSourceManager; _lyricResolver = lyricResolver; _lyricManager = lyricManager; + _mediaStreamRepository = mediaStreamRepository; ATL.Settings.DisplayValueSeparator = InternalValueSeparator; ATL.Settings.UseFileNameWhenNoTitle = false; ATL.Settings.ID3v2_separatev2v3Values = false; @@ -149,7 +153,7 @@ namespace MediaBrowser.Providers.MediaInfo audio.HasLyrics = mediaStreams.Any(s => s.Type == MediaStreamType.Lyric); - _itemRepo.SaveMediaStreams(audio.Id, mediaStreams, cancellationToken); + _mediaStreamRepository.SaveMediaStreams(audio.Id, mediaStreams, cancellationToken); } /// diff --git a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs index d1c0ddb375..bfe4f3300f 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs @@ -74,7 +74,7 @@ namespace MediaBrowser.Providers.MediaInfo return GetImage((Audio)item, imageStreams, cancellationToken); } - private async Task GetImage(Audio item, List imageStreams, CancellationToken cancellationToken) + private async Task GetImage(Audio item, IReadOnlyList imageStreams, CancellationToken cancellationToken) { var path = GetAudioImagePath(item); diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 62c5909441..301555eefa 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -31,6 +31,7 @@ namespace MediaBrowser.Providers.MediaInfo public class FFProbeVideoInfo { private readonly ILogger _logger; + private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaEncoder _mediaEncoder; private readonly IItemRepository _itemRepo; private readonly IBlurayExaminer _blurayExaminer; @@ -42,7 +43,8 @@ namespace MediaBrowser.Providers.MediaInfo private readonly ILibraryManager _libraryManager; private readonly AudioResolver _audioResolver; private readonly SubtitleResolver _subtitleResolver; - private readonly IMediaSourceManager _mediaSourceManager; + private readonly IMediaAttachmentRepository _mediaAttachmentRepository; + private readonly IMediaStreamRepository _mediaStreamRepository; public FFProbeVideoInfo( ILogger logger, @@ -57,7 +59,9 @@ namespace MediaBrowser.Providers.MediaInfo IChapterRepository chapterManager, ILibraryManager libraryManager, AudioResolver audioResolver, - SubtitleResolver subtitleResolver) + SubtitleResolver subtitleResolver, + IMediaAttachmentRepository mediaAttachmentRepository, + IMediaStreamRepository mediaStreamRepository) { _logger = logger; _mediaSourceManager = mediaSourceManager; @@ -72,6 +76,9 @@ namespace MediaBrowser.Providers.MediaInfo _libraryManager = libraryManager; _audioResolver = audioResolver; _subtitleResolver = subtitleResolver; + _mediaAttachmentRepository = mediaAttachmentRepository; + _mediaStreamRepository = mediaStreamRepository; + _mediaStreamRepository = mediaStreamRepository; } public async Task ProbeVideo( @@ -267,11 +274,11 @@ namespace MediaBrowser.Providers.MediaInfo video.HasSubtitles = mediaStreams.Any(i => i.Type == MediaStreamType.Subtitle); - _itemRepo.SaveMediaStreams(video.Id, mediaStreams, cancellationToken); + _mediaStreamRepository.SaveMediaStreams(video.Id, mediaStreams, cancellationToken); if (mediaAttachments.Any()) { - _itemRepo.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken); + _mediaAttachmentRepository.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken); } if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs index f5e9dddcfc..1c2f8b9134 100644 --- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs @@ -67,6 +67,8 @@ namespace MediaBrowser.Providers.MediaInfo /// Instance of the interface. /// The . /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. public ProbeProvider( IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, @@ -81,7 +83,9 @@ namespace MediaBrowser.Providers.MediaInfo IFileSystem fileSystem, ILoggerFactory loggerFactory, NamingOptions namingOptions, - ILyricManager lyricManager) + ILyricManager lyricManager, + IMediaAttachmentRepository mediaAttachmentRepository, + IMediaStreamRepository mediaStreamRepository) { _logger = loggerFactory.CreateLogger(); _audioResolver = new AudioResolver(loggerFactory.CreateLogger(), localization, mediaEncoder, fileSystem, namingOptions); @@ -101,7 +105,9 @@ namespace MediaBrowser.Providers.MediaInfo chapterManager, libraryManager, _audioResolver, - _subtitleResolver); + _subtitleResolver, + mediaAttachmentRepository, + mediaStreamRepository); _audioProber = new AudioFileProber( loggerFactory.CreateLogger(), @@ -110,7 +116,8 @@ namespace MediaBrowser.Providers.MediaInfo itemRepo, libraryManager, _lyricResolver, - lyricManager); + lyricManager, + mediaStreamRepository); } /// diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs index 20fb4dab9c..227f310255 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs @@ -31,7 +31,7 @@ namespace MediaBrowser.Providers.MediaInfo public async Task> DownloadSubtitles( Video video, - List mediaStreams, + IReadOnlyList mediaStreams, bool skipIfEmbeddedSubtitlesPresent, bool skipIfAudioTrackMatches, bool requirePerfectMatch, @@ -68,7 +68,7 @@ namespace MediaBrowser.Providers.MediaInfo public Task DownloadSubtitles( Video video, - List mediaStreams, + IReadOnlyList mediaStreams, bool skipIfEmbeddedSubtitlesPresent, bool skipIfAudioTrackMatches, bool requirePerfectMatch, @@ -120,7 +120,7 @@ namespace MediaBrowser.Providers.MediaInfo private async Task DownloadSubtitles( Video video, - List mediaStreams, + IReadOnlyList mediaStreams, bool skipIfEmbeddedSubtitlesPresent, bool skipIfAudioTrackMatches, bool requirePerfectMatch, diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs index a39bd16cea..25698d8cb5 100644 --- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs +++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs @@ -47,11 +47,11 @@ namespace MediaBrowser.Providers.Music protected override bool EnableUpdatingStudiosFromChildren => true; /// - protected override IList GetChildrenForMetadataUpdates(MusicAlbum item) + protected override IReadOnlyList GetChildrenForMetadataUpdates(MusicAlbum item) => item.GetRecursiveChildren(i => i is Audio); /// - protected override ItemUpdateType UpdateMetadataFromChildren(MusicAlbum item, IList children, bool isFullRefresh, ItemUpdateType currentUpdateType) + protected override ItemUpdateType UpdateMetadataFromChildren(MusicAlbum item, IReadOnlyList children, bool isFullRefresh, ItemUpdateType currentUpdateType) { var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType); diff --git a/MediaBrowser.Providers/Music/ArtistMetadataService.cs b/MediaBrowser.Providers/Music/ArtistMetadataService.cs index 1f342c0db1..8af6de9259 100644 --- a/MediaBrowser.Providers/Music/ArtistMetadataService.cs +++ b/MediaBrowser.Providers/Music/ArtistMetadataService.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System.Collections.Generic; +using System.Collections.Immutable; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -28,7 +29,7 @@ namespace MediaBrowser.Providers.Music protected override bool EnableUpdatingGenresFromChildren => true; /// - protected override IList GetChildrenForMetadataUpdates(MusicArtist item) + protected override IReadOnlyList GetChildrenForMetadataUpdates(MusicArtist item) { return item.IsAccessedByName ? item.GetTaggedItems(new InternalItemsQuery @@ -36,7 +37,7 @@ namespace MediaBrowser.Providers.Music Recursive = true, IsFolder = false }) - : item.GetRecursiveChildren(i => i is IHasArtist && !i.IsFolder); + : item.GetRecursiveChildren(i => i is IHasArtist && !i.IsFolder).ToImmutableArray(); } } } diff --git a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs index 43889bfbf5..7be54453f8 100644 --- a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs +++ b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs @@ -36,7 +36,7 @@ namespace MediaBrowser.Providers.Playlists protected override bool EnableUpdatingStudiosFromChildren => true; /// - protected override IList GetChildrenForMetadataUpdates(Playlist item) + protected override IReadOnlyList GetChildrenForMetadataUpdates(Playlist item) => item.GetLinkedChildren(); /// diff --git a/MediaBrowser.Providers/TV/SeasonMetadataService.cs b/MediaBrowser.Providers/TV/SeasonMetadataService.cs index 8b690193ee..b27ccaa6a3 100644 --- a/MediaBrowser.Providers/TV/SeasonMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeasonMetadataService.cs @@ -80,11 +80,11 @@ namespace MediaBrowser.Providers.TV } /// - protected override IList GetChildrenForMetadataUpdates(Season item) + protected override IReadOnlyList GetChildrenForMetadataUpdates(Season item) => item.GetEpisodes(); /// - protected override ItemUpdateType UpdateMetadataFromChildren(Season item, IList children, bool isFullRefresh, ItemUpdateType currentUpdateType) + protected override ItemUpdateType UpdateMetadataFromChildren(Season item, IReadOnlyList children, bool isFullRefresh, ItemUpdateType currentUpdateType) { var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType); @@ -96,7 +96,7 @@ namespace MediaBrowser.Providers.TV return updateType; } - private ItemUpdateType SaveIsVirtualItem(Season item, IList episodes) + private ItemUpdateType SaveIsVirtualItem(Season item, IReadOnlyList episodes) { var isVirtualItem = item.LocationType == LocationType.Virtual && (episodes.Count == 0 || episodes.All(i => i.LocationType == LocationType.Virtual)); diff --git a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs index 813d75f6c1..4cd676be12 100644 --- a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs @@ -67,7 +67,7 @@ namespace MediaBrowser.XbmcMetadata.Savers AddAlbums(albums, writer); } - private void AddAlbums(IList albums, XmlWriter writer) + private void AddAlbums(IReadOnlyList albums, XmlWriter writer) { foreach (var album in albums) { diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index 79e9e7503c..51c5a20803 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -914,7 +914,7 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteEndElement(); } - private void AddActors(List people, XmlWriter writer, ILibraryManager libraryManager, bool saveImagePath) + private void AddActors(IReadOnlyList people, XmlWriter writer, ILibraryManager libraryManager, bool saveImagePath) { foreach (var person in people) { -- cgit v1.2.3 From 3e7ce5e1df9c6820a6dfd4c23aea29eed83b43ba Mon Sep 17 00:00:00 2001 From: JPVenson Date: Thu, 10 Oct 2024 00:57:19 +0000 Subject: Removed obsolete Score and Similiarity values for search --- Jellyfin.Api/Controllers/LibraryController.cs | 2 -- Jellyfin.Api/Controllers/MoviesController.cs | 1 - Jellyfin.Server.Implementations/Item/BaseItemRepository.cs | 1 - MediaBrowser.Controller/Entities/InternalItemsQuery.cs | 5 ----- 4 files changed, 9 deletions(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index b2d75d5a38..72129a5851 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -780,11 +780,9 @@ public class LibraryController : BaseJellyfinApiController Genres = item.Genres, Limit = limit, IncludeItemTypes = includeItemTypes.ToArray(), - SimilarTo = item, DtoOptions = dtoOptions, EnableTotalRecordCount = !isMovie ?? true, EnableGroupByMetadataKey = isMovie ?? false, - MinSimilarityScore = 2 // A remnant from album/artist scoring }; // ExcludeArtistIds diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index c2bdf71c5a..ae67b6710c 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -277,7 +277,6 @@ public class MoviesController : BaseJellyfinApiController Limit = itemLimit, IncludeItemTypes = itemTypes.ToArray(), IsMovie = true, - SimilarTo = item, EnableGroupByMetadataKey = true, DtoOptions = dtoOptions }); diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index d82de097cd..86c6820275 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1708,7 +1708,6 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr query = query.Where(e => e.Type == returnType && e.ItemValues!.Any(f => e.CleanName == f.CleanValue && itemValueTypes.Any(w => (ItemValueType)w == f.Type))); if (filter.OrderBy.Count != 0 - || filter.SimilarTo is not null || !string.IsNullOrEmpty(filter.SearchTerm)) { query = ApplyOrder(query, filter); diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 1461a3680a..43f02fb72b 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -37,7 +37,6 @@ namespace MediaBrowser.Controller.Entities IncludeItemTypes = Array.Empty(); ItemIds = Array.Empty(); MediaTypes = Array.Empty(); - MinSimilarityScore = 20; OfficialRatings = Array.Empty(); OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); PersonIds = Array.Empty(); @@ -71,8 +70,6 @@ namespace MediaBrowser.Controller.Entities public User? User { get; set; } - public BaseItem? SimilarTo { get; set; } - public bool? IsFolder { get; set; } public bool? IsFavorite { get; set; } @@ -295,8 +292,6 @@ namespace MediaBrowser.Controller.Entities public DtoOptions DtoOptions { get; set; } - public int MinSimilarityScore { get; set; } - public string? HasNoAudioTrackWithLanguage { get; set; } public string? HasNoInternalSubtitleTrackWithLanguage { get; set; } -- cgit v1.2.3 From 439a997fca67ff5fcbca38c87dfef5acf04e05e3 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Thu, 10 Oct 2024 18:01:14 +0000 Subject: Readded custom serialisation --- .../Playlists/PlaylistsFolder.cs | 2 + .../Item/BaseItemRepository.cs | 60 +++++++++++++++++++--- .../RequiresSourceSerialisationAttribute.cs | 11 ++++ .../Entities/Audio/MusicAlbum.cs | 1 + .../Entities/Audio/MusicArtist.cs | 1 + .../Entities/Audio/MusicGenre.cs | 1 + MediaBrowser.Controller/Entities/AudioBook.cs | 1 + MediaBrowser.Controller/Entities/Book.cs | 1 + MediaBrowser.Controller/Entities/Genre.cs | 1 + MediaBrowser.Controller/Entities/Person.cs | 1 + MediaBrowser.Controller/Entities/PhotoAlbum.cs | 1 + MediaBrowser.Controller/Entities/Studio.cs | 1 + MediaBrowser.Controller/Entities/TV/Season.cs | 2 + MediaBrowser.Controller/Entities/Year.cs | 1 + MediaBrowser.Controller/LiveTv/LiveTvProgram.cs | 1 + 15 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 MediaBrowser.Common/RequiresSourceSerialisationAttribute.cs (limited to 'MediaBrowser.Controller/Entities') diff --git a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs index f65d609c71..db3aeaaf31 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs @@ -5,12 +5,14 @@ using System.Linq; using System.Text.Json.Serialization; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using MediaBrowser.Common; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Playlists; using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Playlists { + [RequiresSourceSerialisation] public class PlaylistsFolder : BasePluginFolder { public PlaylistsFolder() diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 7e7873f6d4..f92f526bc7 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -3,14 +3,21 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; +using System.IO; using System.Linq; using System.Linq.Expressions; +using System.Reflection; using System.Text; +using System.Text.Json; using System.Threading; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; +using Jellyfin.Extensions.Json; +using MediaBrowser.Common; using MediaBrowser.Controller; +using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.TV; @@ -21,7 +28,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; using Microsoft.EntityFrameworkCore; -using SQLitePCL; +using Microsoft.Extensions.Logging; using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem; using BaseItemEntity = Jellyfin.Data.Entities.BaseItemEntity; #pragma warning disable RS0030 // Do not use banned APIs @@ -37,7 +44,14 @@ namespace Jellyfin.Server.Implementations.Item; /// The db factory. /// The Application host. /// The static type lookup. -public sealed class BaseItemRepository(IDbContextFactory dbProvider, IServerApplicationHost appHost, IItemTypeLookup itemTypeLookup) +/// The server Configuration manager. +/// System logger. +public sealed class BaseItemRepository( + IDbContextFactory dbProvider, + IServerApplicationHost appHost, + IItemTypeLookup itemTypeLookup, + IServerConfigurationManager serverConfigurationManager, + ILogger logger) : IItemRepository, IDisposable { /// @@ -244,7 +258,7 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr } } - result.Items = dbQuery.ToList().Select(DeserialiseBaseItem).ToImmutableArray(); + result.Items = dbQuery.ToList().Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToImmutableArray(); result.StartIndex = filter.StartIndex ?? 0; return result; } @@ -272,7 +286,7 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr } } - return dbQuery.ToList().Select(DeserialiseBaseItem).ToImmutableArray(); + return dbQuery.ToList().Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToImmutableArray(); } /// @@ -1675,10 +1689,42 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr return query.Select(e => e.ItemValue.CleanValue).ToImmutableArray(); } - private BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity) + private bool TypeRequiresDeserialization(Type type) + { + if (serverConfigurationManager.Configuration.SkipDeserializationForBasicTypes) + { + if (type == typeof(Channel) + || type == typeof(UserRootFolder)) + { + return false; + } + } + + return type.GetCustomAttribute() == null; + } + + private BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false) { var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialise unkown type."); - var dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialise unkown type."); + BaseItemDto? dto = null; + if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization) + { + try + { + using var dataAsStream = new MemoryStream(Encoding.UTF8.GetBytes(baseItemEntity.Data!)); + dto = JsonSerializer.Deserialize(dataAsStream, type, JsonDefaults.Options) as BaseItemDto; + } + catch (JsonException ex) + { + logger.LogError(ex, "Error deserializing item with JSON: {Data}", baseItemEntity.Data); + } + } + + if (dto is null) + { + dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialise unkown type."); + } + return Map(baseItemEntity, dto); } @@ -1764,7 +1810,7 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr result.StartIndex = filter.StartIndex ?? 0; result.Items = resultQuery.ToImmutableArray().Select(e => { - return (DeserialiseBaseItem(e.item), e.itemCount); + return (DeserialiseBaseItem(e.item, filter.SkipDeserialization), e.itemCount); }).ToImmutableArray(); return result; diff --git a/MediaBrowser.Common/RequiresSourceSerialisationAttribute.cs b/MediaBrowser.Common/RequiresSourceSerialisationAttribute.cs new file mode 100644 index 0000000000..b22e7cba17 --- /dev/null +++ b/MediaBrowser.Common/RequiresSourceSerialisationAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace MediaBrowser.Common; + +/// +/// Marks a BaseItem as needing custom serialisation from the Data field of the db. +/// +[System.AttributeUsage(System.AttributeTargets.Class, Inherited = true, AllowMultiple = false)] +public sealed class RequiresSourceSerialisationAttribute : System.Attribute +{ +} diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index a0aae8769c..f3873775b9 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -21,6 +21,7 @@ namespace MediaBrowser.Controller.Entities.Audio /// /// Class MusicAlbum. /// + [Common.RequiresSourceSerialisation] public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo, IMetadataContainer { public MusicAlbum() diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 6d3249399b..5375509256 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -21,6 +21,7 @@ namespace MediaBrowser.Controller.Entities.Audio /// /// Class MusicArtist. /// + [Common.RequiresSourceSerialisation] public class MusicArtist : Folder, IItemByName, IHasMusicGenres, IHasDualAccess, IHasLookupInfo { [JsonIgnore] diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs index 80f3902be7..65669e6804 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs @@ -14,6 +14,7 @@ namespace MediaBrowser.Controller.Entities.Audio /// /// Class MusicGenre. /// + [Common.RequiresSourceSerialisation] public class MusicGenre : BaseItem, IItemByName { [JsonIgnore] diff --git a/MediaBrowser.Controller/Entities/AudioBook.cs b/MediaBrowser.Controller/Entities/AudioBook.cs index 782481fbcd..666bf2a750 100644 --- a/MediaBrowser.Controller/Entities/AudioBook.cs +++ b/MediaBrowser.Controller/Entities/AudioBook.cs @@ -9,6 +9,7 @@ using MediaBrowser.Controller.Providers; namespace MediaBrowser.Controller.Entities { + [Common.RequiresSourceSerialisation] public class AudioBook : Audio.Audio, IHasSeries, IHasLookupInfo { [JsonIgnore] diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs index 66dea1084c..5187669373 100644 --- a/MediaBrowser.Controller/Entities/Book.cs +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -10,6 +10,7 @@ using MediaBrowser.Controller.Providers; namespace MediaBrowser.Controller.Entities { + [Common.RequiresSourceSerialisation] public class Book : BaseItem, IHasLookupInfo, IHasSeries { public Book() diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs index e5353d7bd9..6ec78a270e 100644 --- a/MediaBrowser.Controller/Entities/Genre.cs +++ b/MediaBrowser.Controller/Entities/Genre.cs @@ -14,6 +14,7 @@ namespace MediaBrowser.Controller.Entities /// /// Class Genre. /// + [Common.RequiresSourceSerialisation] public class Genre : BaseItem, IItemByName { /// diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs index b0933d23f4..5cc4d322f7 100644 --- a/MediaBrowser.Controller/Entities/Person.cs +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -14,6 +14,7 @@ namespace MediaBrowser.Controller.Entities /// /// This is the full Person object that can be retrieved with all of it's data. /// + [Common.RequiresSourceSerialisation] public class Person : BaseItem, IItemByName, IHasLookupInfo { /// diff --git a/MediaBrowser.Controller/Entities/PhotoAlbum.cs b/MediaBrowser.Controller/Entities/PhotoAlbum.cs index a7ecb9061c..5b31b4f116 100644 --- a/MediaBrowser.Controller/Entities/PhotoAlbum.cs +++ b/MediaBrowser.Controller/Entities/PhotoAlbum.cs @@ -4,6 +4,7 @@ using System.Text.Json.Serialization; namespace MediaBrowser.Controller.Entities { + [Common.RequiresSourceSerialisation] public class PhotoAlbum : Folder { [JsonIgnore] diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs index b46a3d1bcf..9103b09a95 100644 --- a/MediaBrowser.Controller/Entities/Studio.cs +++ b/MediaBrowser.Controller/Entities/Studio.cs @@ -13,6 +13,7 @@ namespace MediaBrowser.Controller.Entities /// /// Class Studio. /// + [Common.RequiresSourceSerialisation] public class Studio : BaseItem, IItemByName { /// diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 181b9be2bf..8e9f5818d0 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -10,6 +10,7 @@ using System.Text.Json.Serialization; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; +using MediaBrowser.Common; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Querying; @@ -19,6 +20,7 @@ namespace MediaBrowser.Controller.Entities.TV /// /// Class Season. /// + [RequiresSourceSerialisation] public class Season : Folder, IHasSeries, IHasLookupInfo { [JsonIgnore] diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs index 587d7ce7e5..37820296cc 100644 --- a/MediaBrowser.Controller/Entities/Year.cs +++ b/MediaBrowser.Controller/Entities/Year.cs @@ -13,6 +13,7 @@ namespace MediaBrowser.Controller.Entities /// /// Class Year. /// + [Common.RequiresSourceSerialisation] public class Year : BaseItem, IItemByName { [JsonIgnore] diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs index 2ac6f99633..83944f741c 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs @@ -18,6 +18,7 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Controller.LiveTv { + [Common.RequiresSourceSerialisation] public class LiveTvProgram : BaseItem, IHasLookupInfo, IHasStartDate, IHasProgramAttributes { private const string EmbyServiceName = "Emby"; -- cgit v1.2.3 From b73985e04f76924ec91692890687461bcfdb4e11 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Fri, 11 Oct 2024 11:11:15 +0000 Subject: Expanded People architecture and fixed migration --- Jellyfin.Data/Entities/BaseItemEntity.cs | 2 +- Jellyfin.Data/Entities/People.cs | 26 +- Jellyfin.Data/Entities/PeopleBaseItemMap.cs | 44 + .../Item/BaseItemRepository.cs | 9 +- .../Item/PeopleRepository.cs | 47 +- .../JellyfinDbContext.cs | 11 +- ...241011095125_LibraryPeopleMigration.Designer.cs | 1613 ++++++++++++++++++++ .../20241011095125_LibraryPeopleMigration.cs | 152 ++ ...11100757_LibraryPeopleRoleMigration.Designer.cs | 1613 ++++++++++++++++++++ .../20241011100757_LibraryPeopleRoleMigration.cs | 38 + .../Migrations/JellyfinDbModelSnapshot.cs | 51 +- .../PeopleBaseItemMapConfiguration.cs | 22 + .../ModelConfiguration/PeopleConfiguration.cs | 4 +- Jellyfin.Server/Migrations/MigrationRunner.cs | 3 +- .../Migrations/Routines/MigrateLibraryDb.cs | 167 +- MediaBrowser.Controller/Entities/PersonInfo.cs | 6 + 16 files changed, 3708 insertions(+), 100 deletions(-) create mode 100644 Jellyfin.Data/Entities/PeopleBaseItemMap.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20241011095125_LibraryPeopleMigration.Designer.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20241011095125_LibraryPeopleMigration.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20241011100757_LibraryPeopleRoleMigration.Designer.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20241011100757_LibraryPeopleRoleMigration.cs create mode 100644 Jellyfin.Server.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs (limited to 'MediaBrowser.Controller/Entities') diff --git a/Jellyfin.Data/Entities/BaseItemEntity.cs b/Jellyfin.Data/Entities/BaseItemEntity.cs index 7670c18930..a9f9b17934 100644 --- a/Jellyfin.Data/Entities/BaseItemEntity.cs +++ b/Jellyfin.Data/Entities/BaseItemEntity.cs @@ -154,7 +154,7 @@ public class BaseItemEntity public Guid? SeriesId { get; set; } - public ICollection? Peoples { get; set; } + public ICollection? Peoples { get; set; } public ICollection? UserData { get; set; } diff --git a/Jellyfin.Data/Entities/People.cs b/Jellyfin.Data/Entities/People.cs index 8eb23f5e4d..b1834a70d5 100644 --- a/Jellyfin.Data/Entities/People.cs +++ b/Jellyfin.Data/Entities/People.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; namespace Jellyfin.Data.Entities; +#pragma warning disable CA2227 // Collection properties should be read only /// /// People entity. @@ -11,37 +10,22 @@ namespace Jellyfin.Data.Entities; public class People { /// - /// Gets or Sets The ItemId. + /// Gets or Sets the PeopleId. /// - public required Guid ItemId { get; set; } - - /// - /// Gets or Sets Reference Item. - /// - public required BaseItemEntity Item { get; set; } + public required Guid Id { get; set; } /// /// Gets or Sets the Persons Name. /// public required string Name { get; set; } - /// - /// Gets or Sets the Role. - /// - public string? Role { get; set; } - /// /// Gets or Sets the Type. /// public string? PersonType { get; set; } /// - /// Gets or Sets the SortOrder. - /// - public int? SortOrder { get; set; } - - /// - /// Gets or Sets the ListOrder. + /// Gets or Sets the mapping of People to BaseItems. /// - public int? ListOrder { get; set; } + public ICollection? BaseItems { get; set; } } diff --git a/Jellyfin.Data/Entities/PeopleBaseItemMap.cs b/Jellyfin.Data/Entities/PeopleBaseItemMap.cs new file mode 100644 index 0000000000..5ce7300b58 --- /dev/null +++ b/Jellyfin.Data/Entities/PeopleBaseItemMap.cs @@ -0,0 +1,44 @@ +using System; + +namespace Jellyfin.Data.Entities; + +/// +/// Mapping table for People to BaseItems. +/// +public class PeopleBaseItemMap +{ + /// + /// Gets or Sets the SortOrder. + /// + public int? SortOrder { get; set; } + + /// + /// Gets or Sets the ListOrder. + /// + public int? ListOrder { get; set; } + + /// + /// Gets or Sets the Role name the assosiated actor played in the . + /// + public string? Role { get; set; } + + /// + /// Gets or Sets The ItemId. + /// + public required Guid ItemId { get; set; } + + /// + /// Gets or Sets Reference Item. + /// + public required BaseItemEntity Item { get; set; } + + /// + /// Gets or Sets The PeopleId. + /// + public required Guid PeopleId { get; set; } + + /// + /// Gets or Sets Reference People. + /// + public required People People { get; set; } +} diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 208bb41987..36d976a436 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -81,7 +81,8 @@ public sealed class BaseItemRepository( using var context = dbProvider.CreateDbContext(); using var transaction = context.Database.BeginTransaction(); - context.Peoples.Where(e => e.ItemId == id).ExecuteDelete(); + context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete(); + context.Peoples.Where(e => e.BaseItems!.Count == 0).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(); @@ -602,13 +603,13 @@ public sealed class BaseItemRepository( { baseQuery = baseQuery .Where(e => - context.Peoples.Where(w => context.BaseItems.Where(r => filter.PersonIds.Contains(r.Id)).Any(f => f.Name == w.Name)) + context.PeopleBaseItemMap.Where(w => context.BaseItems.Where(r => filter.PersonIds.Contains(r.Id)).Any(f => f.Name == w.People.Name)) .Any(f => f.ItemId == e.Id)); } if (!string.IsNullOrWhiteSpace(filter.Person)) { - baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.Name == filter.Person)); + baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.People.Name == filter.Person)); } if (!string.IsNullOrWhiteSpace(filter.MinSortName)) @@ -934,7 +935,7 @@ public sealed class BaseItemRepository( if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value) { baseQuery = baseQuery - .Where(e => !e.Peoples!.Any(f => f.Name == e.Name)); + .Where(e => !e.Peoples!.Any(f => f.People.Name == e.Name)); } if (filter.Years.Length == 1) diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index 57f0503b9e..dee87f48f9 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -10,6 +10,7 @@ using MediaBrowser.Controller.Persistence; using Microsoft.EntityFrameworkCore; namespace Jellyfin.Server.Implementations.Item; +#pragma warning disable RS0030 // Do not use banned APIs /// /// Manager for handling people. @@ -28,7 +29,7 @@ public class PeopleRepository(IDbContextFactory dbProvider) : using var context = _dbProvider.CreateDbContext(); var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter); - dbQuery = dbQuery.OrderBy(e => e.ListOrder); + // dbQuery = dbQuery.OrderBy(e => e.ListOrder); if (filter.Limit > 0) { dbQuery = dbQuery.Take(filter.Limit); @@ -43,7 +44,7 @@ public class PeopleRepository(IDbContextFactory dbProvider) : using var context = _dbProvider.CreateDbContext(); var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter); - dbQuery = dbQuery.OrderBy(e => e.ListOrder); + // dbQuery = dbQuery.OrderBy(e => e.ListOrder); if (filter.Limit > 0) { dbQuery = dbQuery.Take(filter.Limit); @@ -58,7 +59,29 @@ public class PeopleRepository(IDbContextFactory dbProvider) : using var context = _dbProvider.CreateDbContext(); using var transaction = context.Database.BeginTransaction(); - context.Peoples.Where(e => e.ItemId.Equals(itemId)).ExecuteDelete(); + context.PeopleBaseItemMap.Where(e => e.ItemId == itemId).ExecuteDelete(); + foreach (var item in people) + { + var personEntity = Map(item); + var existingEntity = context.Peoples.FirstOrDefault(e => e.Id == personEntity.Id); + if (existingEntity is null) + { + context.Peoples.Add(personEntity); + existingEntity = personEntity; + } + + context.PeopleBaseItemMap.Add(new PeopleBaseItemMap() + { + Item = null!, + ItemId = itemId, + People = existingEntity, + PeopleId = existingEntity.Id, + ListOrder = item.SortOrder, + SortOrder = item.SortOrder, + Role = item.Role + }); + } + context.Peoples.AddRange(people.Select(Map)); context.SaveChanges(); transaction.Commit(); @@ -68,10 +91,8 @@ public class PeopleRepository(IDbContextFactory dbProvider) : { var personInfo = new PersonInfo() { - ItemId = people.ItemId, + Id = people.Id, Name = people.Name, - Role = people.Role, - SortOrder = people.SortOrder, }; if (Enum.TryParse(people.PersonType, out var kind)) { @@ -85,13 +106,9 @@ public class PeopleRepository(IDbContextFactory dbProvider) : { var personInfo = new People() { - ItemId = people.ItemId, Name = people.Name, - Role = people.Role, - SortOrder = people.SortOrder, PersonType = people.Type.ToString(), - Item = null!, - ListOrder = people.SortOrder + Id = people.Id, }; return personInfo; @@ -108,12 +125,12 @@ public class PeopleRepository(IDbContextFactory dbProvider) : if (!filter.ItemId.IsEmpty()) { - query = query.Where(e => e.ItemId.Equals(filter.ItemId)); + query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.ItemId))); } if (!filter.AppearsInItemId.IsEmpty()) { - query = query.Where(e => context.Peoples.Where(f => f.ItemId.Equals(filter.AppearsInItemId)).Select(e => e.Name).Contains(e.Name)); + query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.AppearsInItemId))); } var queryPersonTypes = filter.PersonTypes.Where(IsValidPersonType).ToList(); @@ -129,9 +146,9 @@ public class PeopleRepository(IDbContextFactory dbProvider) : query = query.Where(e => !queryPersonTypes.Contains(e.PersonType)); } - if (filter.MaxListOrder.HasValue) + if (filter.MaxListOrder.HasValue && !filter.ItemId.IsEmpty()) { - query = query.Where(e => e.ListOrder <= filter.MaxListOrder.Value); + query = query.Where(e => e.BaseItems!.First(w => w.ItemId == filter.ItemId).ListOrder <= filter.MaxListOrder.Value); } if (!string.IsNullOrWhiteSpace(filter.NameContains)) diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs index 284897c994..becfd81a4a 100644 --- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs +++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs @@ -112,7 +112,7 @@ public class JellyfinDbContext(DbContextOptions options, ILog public DbSet Chapters => Set(); /// - /// Gets the containing the user data. + /// Gets the . /// public DbSet ItemValues => Set(); @@ -122,15 +122,20 @@ public class JellyfinDbContext(DbContextOptions options, ILog public DbSet ItemValuesMap => Set(); /// - /// Gets the containing the user data. + /// Gets the . /// public DbSet MediaStreamInfos => Set(); /// - /// Gets the containing the user data. + /// Gets the . /// public DbSet Peoples => Set(); + /// + /// Gets the . + /// + public DbSet PeopleBaseItemMap => Set(); + /// /// Gets the containing the referenced Providers with ids. /// diff --git a/Jellyfin.Server.Implementations/Migrations/20241011095125_LibraryPeopleMigration.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20241011095125_LibraryPeopleMigration.Designer.cs new file mode 100644 index 0000000000..9e33b8effd --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20241011095125_LibraryPeopleMigration.Designer.cs @@ -0,0 +1,1613 @@ +// +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("20241011095125_LibraryPeopleMigration")] + partial class LibraryPeopleMigration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ParentItemId") + .HasColumnType("TEXT"); + + b.Property("BaseItemEntityId") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ParentItemId"); + + b.HasIndex("BaseItemEntityId"); + + b.HasIndex("ParentItemId"); + + b.ToTable("AncestorIds"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Filename") + .HasColumnType("TEXT"); + + b.Property("MimeType") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "Index"); + + b.ToTable("AttachmentStreamInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("AlbumArtists") + .HasColumnType("TEXT"); + + b.Property("Artists") + .HasColumnType("TEXT"); + + b.Property("Audio") + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("TEXT"); + + b.Property("CleanName") + .HasColumnType("TEXT"); + + b.Property("CommunityRating") + .HasColumnType("REAL"); + + b.Property("CriticRating") + .HasColumnType("REAL"); + + b.Property("CustomRating") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastMediaAdded") + .HasColumnType("TEXT"); + + b.Property("DateLastRefreshed") + .HasColumnType("TEXT"); + + b.Property("DateLastSaved") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("EpisodeTitle") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasColumnType("TEXT"); + + b.Property("ExternalSeriesId") + .HasColumnType("TEXT"); + + b.Property("ExternalServiceId") + .HasColumnType("TEXT"); + + b.Property("ExtraIds") + .HasColumnType("TEXT"); + + b.Property("ExtraType") + .HasColumnType("INTEGER"); + + b.Property("ForcedSortName") + .HasColumnType("TEXT"); + + b.Property("Genres") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IndexNumber") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingValue") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("IsInMixedFolder") + .HasColumnType("INTEGER"); + + b.Property("IsLocked") + .HasColumnType("INTEGER"); + + b.Property("IsMovie") + .HasColumnType("INTEGER"); + + b.Property("IsRepeat") + .HasColumnType("INTEGER"); + + b.Property("IsSeries") + .HasColumnType("INTEGER"); + + b.Property("IsVirtualItem") + .HasColumnType("INTEGER"); + + b.Property("LUFS") + .HasColumnType("REAL"); + + b.Property("MediaType") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizationGain") + .HasColumnType("REAL"); + + b.Property("OfficialRating") + .HasColumnType("TEXT"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ParentIndexNumber") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataCountryCode") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataLanguage") + .HasColumnType("TEXT"); + + b.Property("PremiereDate") + .HasColumnType("TEXT"); + + b.Property("PresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("PrimaryVersionId") + .HasColumnType("TEXT"); + + b.Property("ProductionLocations") + .HasColumnType("TEXT"); + + b.Property("ProductionYear") + .HasColumnType("INTEGER"); + + b.Property("RunTimeTicks") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("TEXT"); + + b.Property("SeasonName") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("SeriesName") + .HasColumnType("TEXT"); + + b.Property("SeriesPresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("ShowId") + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Studios") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("TopParentId") + .HasColumnType("TEXT"); + + b.Property("TotalBitrate") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UnratedType") + .HasColumnType("TEXT"); + + b.Property("UserDataKey") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("Path"); + + b.HasIndex("PresentationUniqueKey"); + + b.HasIndex("TopParentId", "Id"); + + b.HasIndex("UserDataKey", "Type"); + + b.HasIndex("Type", "TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "PresentationUniqueKey"); + + b.HasIndex("Type", "TopParentId", "StartDate"); + + b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem"); + + b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName"); + + b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.ToTable("BaseItems"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Blurhash") + .HasColumnType("BLOB"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("ImageType") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemMetadataFields"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ProviderId"); + + b.HasIndex("ProviderId", "ProviderValue", "ItemId"); + + b.ToTable("BaseItemProviders"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemTrailerTypes"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ChapterIndex") + .HasColumnType("INTEGER"); + + b.Property("ImageDateModified") + .HasColumnType("TEXT"); + + b.Property("ImagePath") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("StartPositionTicks") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "ChapterIndex"); + + b.ToTable("Chapters"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => + { + b.Property("ItemValueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CleanValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId"); + + b.HasIndex("Type", "CleanValue"); + + b.ToTable("ItemValues"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => + { + b.Property("ItemValueId") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("ItemValuesMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EndTicks") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("SegmentProviderId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartTicks") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSegments"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("AspectRatio") + .HasColumnType("TEXT"); + + b.Property("AverageFrameRate") + .HasColumnType("REAL"); + + b.Property("BitDepth") + .HasColumnType("INTEGER"); + + b.Property("BitRate") + .HasColumnType("INTEGER"); + + b.Property("BlPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("ChannelLayout") + .HasColumnType("TEXT"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CodecTimeBase") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ColorPrimaries") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ColorSpace") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ColorTransfer") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DvBlSignalCompatibilityId") + .HasColumnType("INTEGER"); + + b.Property("DvLevel") + .HasColumnType("INTEGER"); + + b.Property("DvProfile") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMajor") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMinor") + .HasColumnType("INTEGER"); + + b.Property("ElPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IsAnamorphic") + .HasColumnType("INTEGER"); + + b.Property("IsAvc") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsExternal") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("IsHearingImpaired") + .HasColumnType("INTEGER"); + + b.Property("IsInterlaced") + .HasColumnType("INTEGER"); + + b.Property("KeyFrames") + .HasColumnType("TEXT"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("REAL"); + + b.Property("NalLengthSize") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PixelFormat") + .HasColumnType("TEXT"); + + b.Property("Profile") + .HasColumnType("TEXT"); + + b.Property("RealFrameRate") + .HasColumnType("REAL"); + + b.Property("RefFrames") + .HasColumnType("INTEGER"); + + b.Property("Rotation") + .HasColumnType("INTEGER"); + + b.Property("RpuPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("SampleRate") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .HasColumnType("INTEGER"); + + b.Property("TimeBase") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "StreamIndex"); + + b.HasIndex("StreamIndex"); + + b.HasIndex("StreamType"); + + b.HasIndex("StreamIndex", "StreamType"); + + b.HasIndex("StreamIndex", "StreamType", "Language"); + + b.ToTable("MediaStreamInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.People", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PersonType") + .HasColumnType("TEXT"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Peoples"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("PeopleId") + .HasColumnType("TEXT"); + + b.Property("ListOrder") + .HasColumnType("INTEGER"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.HasIndex("ItemId", "ListOrder"); + + b.HasIndex("ItemId", "SortOrder"); + + b.ToTable("PeopleBaseItemMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("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("Key") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("AudioStreamIndex") + .HasColumnType("INTEGER"); + + b.Property("BaseItemEntityId") + .HasColumnType("TEXT"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER"); + + b.Property("LastPlayedDate") + .HasColumnType("TEXT"); + + b.Property("Likes") + .HasColumnType("INTEGER"); + + b.Property("PlayCount") + .HasColumnType("INTEGER"); + + b.Property("PlaybackPositionTicks") + .HasColumnType("INTEGER"); + + b.Property("Played") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("SubtitleStreamIndex") + .HasColumnType("INTEGER"); + + b.HasKey("Key", "UserId"); + + b.HasIndex("BaseItemEntityId"); + + b.HasIndex("UserId"); + + b.HasIndex("Key", "UserId", "IsFavorite"); + + b.HasIndex("Key", "UserId", "LastPlayedDate"); + + b.HasIndex("Key", "UserId", "PlaybackPositionTicks"); + + b.HasIndex("Key", "UserId", "Played"); + + b.ToTable("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", null) + .WithMany("AncestorIds") + .HasForeignKey("BaseItemEntityId"); + + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem") + .WithMany() + .HasForeignKey("ParentItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ParentItem"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Images") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("LockedFields") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Provider") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("TrailerTypes") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Chapters") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("ItemValues") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue") + .WithMany("BaseItemsMap") + .HasForeignKey("ItemValueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ItemValue"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("MediaStreams") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Peoples") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.People", "People") + .WithMany("BaseItems") + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("People"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", null) + .WithMany("UserData") + .HasForeignKey("BaseItemEntityId"); + + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => + { + b.Navigation("AncestorIds"); + + b.Navigation("Chapters"); + + b.Navigation("Images"); + + b.Navigation("ItemValues"); + + b.Navigation("LockedFields"); + + b.Navigation("MediaStreams"); + + b.Navigation("Peoples"); + + b.Navigation("Provider"); + + b.Navigation("TrailerTypes"); + + b.Navigation("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => + { + b.Navigation("BaseItemsMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.People", b => + { + b.Navigation("BaseItems"); + }); + + 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/20241011095125_LibraryPeopleMigration.cs b/Jellyfin.Server.Implementations/Migrations/20241011095125_LibraryPeopleMigration.cs new file mode 100644 index 0000000000..2541260c92 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20241011095125_LibraryPeopleMigration.cs @@ -0,0 +1,152 @@ +using System; +using System.Runtime.CompilerServices; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// + public partial class LibraryPeopleMigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Peoples_BaseItems_ItemId", + table: "Peoples"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Peoples", + table: "Peoples"); + + migrationBuilder.DropIndex( + name: "IX_Peoples_ItemId_ListOrder", + table: "Peoples"); + + migrationBuilder.DropColumn( + name: "ListOrder", + table: "Peoples"); + + migrationBuilder.DropColumn( + name: "SortOrder", + table: "Peoples"); + + migrationBuilder.RenameColumn( + name: "ItemId", + table: "Peoples", + newName: "Id"); + + migrationBuilder.AlterColumn( + name: "Role", + table: "Peoples", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Peoples", + table: "Peoples", + column: "Id"); + + migrationBuilder.CreateTable( + name: "PeopleBaseItemMap", + columns: table => new + { + ItemId = table.Column(type: "TEXT", nullable: false), + PeopleId = table.Column(type: "TEXT", nullable: false), + SortOrder = table.Column(type: "INTEGER", nullable: true), + ListOrder = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PeopleBaseItemMap", x => new { x.ItemId, x.PeopleId }); + table.ForeignKey( + name: "FK_PeopleBaseItemMap_BaseItems_ItemId", + column: x => x.ItemId, + principalTable: "BaseItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PeopleBaseItemMap_Peoples_PeopleId", + column: x => x.PeopleId, + principalTable: "Peoples", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_PeopleBaseItemMap_ItemId_ListOrder", + table: "PeopleBaseItemMap", + columns: new[] { "ItemId", "ListOrder" }); + + migrationBuilder.CreateIndex( + name: "IX_PeopleBaseItemMap_ItemId_SortOrder", + table: "PeopleBaseItemMap", + columns: new[] { "ItemId", "SortOrder" }); + + migrationBuilder.CreateIndex( + name: "IX_PeopleBaseItemMap_PeopleId", + table: "PeopleBaseItemMap", + column: "PeopleId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PeopleBaseItemMap"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Peoples", + table: "Peoples"); + + migrationBuilder.RenameColumn( + name: "Id", + table: "Peoples", + newName: "ItemId"); + + migrationBuilder.AlterColumn( + name: "Role", + table: "Peoples", + type: "TEXT", + nullable: false, + defaultValue: string.Empty, + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "ListOrder", + table: "Peoples", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "SortOrder", + table: "Peoples", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddPrimaryKey( + name: "PK_Peoples", + table: "Peoples", + columns: new[] { "ItemId", "Role", "ListOrder" }); + + migrationBuilder.CreateIndex( + name: "IX_Peoples_ItemId_ListOrder", + table: "Peoples", + columns: new[] { "ItemId", "ListOrder" }); + + migrationBuilder.AddForeignKey( + name: "FK_Peoples_BaseItems_ItemId", + table: "Peoples", + column: "ItemId", + principalTable: "BaseItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20241011100757_LibraryPeopleRoleMigration.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20241011100757_LibraryPeopleRoleMigration.Designer.cs new file mode 100644 index 0000000000..7a754d78d2 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20241011100757_LibraryPeopleRoleMigration.Designer.cs @@ -0,0 +1,1613 @@ +// +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("20241011100757_LibraryPeopleRoleMigration")] + partial class LibraryPeopleRoleMigration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ParentItemId") + .HasColumnType("TEXT"); + + b.Property("BaseItemEntityId") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ParentItemId"); + + b.HasIndex("BaseItemEntityId"); + + b.HasIndex("ParentItemId"); + + b.ToTable("AncestorIds"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Filename") + .HasColumnType("TEXT"); + + b.Property("MimeType") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "Index"); + + b.ToTable("AttachmentStreamInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("AlbumArtists") + .HasColumnType("TEXT"); + + b.Property("Artists") + .HasColumnType("TEXT"); + + b.Property("Audio") + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("TEXT"); + + b.Property("CleanName") + .HasColumnType("TEXT"); + + b.Property("CommunityRating") + .HasColumnType("REAL"); + + b.Property("CriticRating") + .HasColumnType("REAL"); + + b.Property("CustomRating") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastMediaAdded") + .HasColumnType("TEXT"); + + b.Property("DateLastRefreshed") + .HasColumnType("TEXT"); + + b.Property("DateLastSaved") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("EpisodeTitle") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasColumnType("TEXT"); + + b.Property("ExternalSeriesId") + .HasColumnType("TEXT"); + + b.Property("ExternalServiceId") + .HasColumnType("TEXT"); + + b.Property("ExtraIds") + .HasColumnType("TEXT"); + + b.Property("ExtraType") + .HasColumnType("INTEGER"); + + b.Property("ForcedSortName") + .HasColumnType("TEXT"); + + b.Property("Genres") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IndexNumber") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingValue") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("IsInMixedFolder") + .HasColumnType("INTEGER"); + + b.Property("IsLocked") + .HasColumnType("INTEGER"); + + b.Property("IsMovie") + .HasColumnType("INTEGER"); + + b.Property("IsRepeat") + .HasColumnType("INTEGER"); + + b.Property("IsSeries") + .HasColumnType("INTEGER"); + + b.Property("IsVirtualItem") + .HasColumnType("INTEGER"); + + b.Property("LUFS") + .HasColumnType("REAL"); + + b.Property("MediaType") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizationGain") + .HasColumnType("REAL"); + + b.Property("OfficialRating") + .HasColumnType("TEXT"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ParentIndexNumber") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataCountryCode") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataLanguage") + .HasColumnType("TEXT"); + + b.Property("PremiereDate") + .HasColumnType("TEXT"); + + b.Property("PresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("PrimaryVersionId") + .HasColumnType("TEXT"); + + b.Property("ProductionLocations") + .HasColumnType("TEXT"); + + b.Property("ProductionYear") + .HasColumnType("INTEGER"); + + b.Property("RunTimeTicks") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("TEXT"); + + b.Property("SeasonName") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("SeriesName") + .HasColumnType("TEXT"); + + b.Property("SeriesPresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("ShowId") + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Studios") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("TopParentId") + .HasColumnType("TEXT"); + + b.Property("TotalBitrate") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UnratedType") + .HasColumnType("TEXT"); + + b.Property("UserDataKey") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("Path"); + + b.HasIndex("PresentationUniqueKey"); + + b.HasIndex("TopParentId", "Id"); + + b.HasIndex("UserDataKey", "Type"); + + b.HasIndex("Type", "TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "PresentationUniqueKey"); + + b.HasIndex("Type", "TopParentId", "StartDate"); + + b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem"); + + b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName"); + + b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.ToTable("BaseItems"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Blurhash") + .HasColumnType("BLOB"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("ImageType") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemMetadataFields"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ProviderId"); + + b.HasIndex("ProviderId", "ProviderValue", "ItemId"); + + b.ToTable("BaseItemProviders"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemTrailerTypes"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ChapterIndex") + .HasColumnType("INTEGER"); + + b.Property("ImageDateModified") + .HasColumnType("TEXT"); + + b.Property("ImagePath") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("StartPositionTicks") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "ChapterIndex"); + + b.ToTable("Chapters"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => + { + b.Property("ItemValueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CleanValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId"); + + b.HasIndex("Type", "CleanValue"); + + b.ToTable("ItemValues"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => + { + b.Property("ItemValueId") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("ItemValuesMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EndTicks") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("SegmentProviderId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartTicks") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSegments"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("AspectRatio") + .HasColumnType("TEXT"); + + b.Property("AverageFrameRate") + .HasColumnType("REAL"); + + b.Property("BitDepth") + .HasColumnType("INTEGER"); + + b.Property("BitRate") + .HasColumnType("INTEGER"); + + b.Property("BlPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("ChannelLayout") + .HasColumnType("TEXT"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CodecTimeBase") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ColorPrimaries") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ColorSpace") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ColorTransfer") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DvBlSignalCompatibilityId") + .HasColumnType("INTEGER"); + + b.Property("DvLevel") + .HasColumnType("INTEGER"); + + b.Property("DvProfile") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMajor") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMinor") + .HasColumnType("INTEGER"); + + b.Property("ElPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IsAnamorphic") + .HasColumnType("INTEGER"); + + b.Property("IsAvc") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsExternal") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("IsHearingImpaired") + .HasColumnType("INTEGER"); + + b.Property("IsInterlaced") + .HasColumnType("INTEGER"); + + b.Property("KeyFrames") + .HasColumnType("TEXT"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("REAL"); + + b.Property("NalLengthSize") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PixelFormat") + .HasColumnType("TEXT"); + + b.Property("Profile") + .HasColumnType("TEXT"); + + b.Property("RealFrameRate") + .HasColumnType("REAL"); + + b.Property("RefFrames") + .HasColumnType("INTEGER"); + + b.Property("Rotation") + .HasColumnType("INTEGER"); + + b.Property("RpuPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("SampleRate") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .HasColumnType("INTEGER"); + + b.Property("TimeBase") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "StreamIndex"); + + b.HasIndex("StreamIndex"); + + b.HasIndex("StreamType"); + + b.HasIndex("StreamIndex", "StreamType"); + + b.HasIndex("StreamIndex", "StreamType", "Language"); + + b.ToTable("MediaStreamInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.People", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PersonType") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Peoples"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("PeopleId") + .HasColumnType("TEXT"); + + b.Property("ListOrder") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.HasIndex("ItemId", "ListOrder"); + + b.HasIndex("ItemId", "SortOrder"); + + b.ToTable("PeopleBaseItemMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("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("Key") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("AudioStreamIndex") + .HasColumnType("INTEGER"); + + b.Property("BaseItemEntityId") + .HasColumnType("TEXT"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER"); + + b.Property("LastPlayedDate") + .HasColumnType("TEXT"); + + b.Property("Likes") + .HasColumnType("INTEGER"); + + b.Property("PlayCount") + .HasColumnType("INTEGER"); + + b.Property("PlaybackPositionTicks") + .HasColumnType("INTEGER"); + + b.Property("Played") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("SubtitleStreamIndex") + .HasColumnType("INTEGER"); + + b.HasKey("Key", "UserId"); + + b.HasIndex("BaseItemEntityId"); + + b.HasIndex("UserId"); + + b.HasIndex("Key", "UserId", "IsFavorite"); + + b.HasIndex("Key", "UserId", "LastPlayedDate"); + + b.HasIndex("Key", "UserId", "PlaybackPositionTicks"); + + b.HasIndex("Key", "UserId", "Played"); + + b.ToTable("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", null) + .WithMany("AncestorIds") + .HasForeignKey("BaseItemEntityId"); + + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem") + .WithMany() + .HasForeignKey("ParentItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ParentItem"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Images") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("LockedFields") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Provider") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("TrailerTypes") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Chapters") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("ItemValues") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue") + .WithMany("BaseItemsMap") + .HasForeignKey("ItemValueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ItemValue"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("MediaStreams") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Peoples") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.People", "People") + .WithMany("BaseItems") + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("People"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", null) + .WithMany("UserData") + .HasForeignKey("BaseItemEntityId"); + + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => + { + b.Navigation("AncestorIds"); + + b.Navigation("Chapters"); + + b.Navigation("Images"); + + b.Navigation("ItemValues"); + + b.Navigation("LockedFields"); + + b.Navigation("MediaStreams"); + + b.Navigation("Peoples"); + + b.Navigation("Provider"); + + b.Navigation("TrailerTypes"); + + b.Navigation("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => + { + b.Navigation("BaseItemsMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.People", b => + { + b.Navigation("BaseItems"); + }); + + 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/20241011100757_LibraryPeopleRoleMigration.cs b/Jellyfin.Server.Implementations/Migrations/20241011100757_LibraryPeopleRoleMigration.cs new file mode 100644 index 0000000000..6f0590c13b --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20241011100757_LibraryPeopleRoleMigration.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// + public partial class LibraryPeopleRoleMigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Role", + table: "Peoples"); + + migrationBuilder.AddColumn( + name: "Role", + table: "PeopleBaseItemMap", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Role", + table: "PeopleBaseItemMap"); + + migrationBuilder.AddColumn( + name: "Role", + table: "Peoples", + type: "TEXT", + nullable: true); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index 20d7cf3dda..4a63cd9265 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -910,33 +910,51 @@ namespace Jellyfin.Server.Implementations.Migrations }); modelBuilder.Entity("Jellyfin.Data.Entities.People", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PersonType") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Peoples"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => { b.Property("ItemId") .HasColumnType("TEXT"); - b.Property("Role") + b.Property("PeopleId") .HasColumnType("TEXT"); b.Property("ListOrder") .HasColumnType("INTEGER"); - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PersonType") + b.Property("Role") .HasColumnType("TEXT"); b.Property("SortOrder") .HasColumnType("INTEGER"); - b.HasKey("ItemId", "Role", "ListOrder"); + b.HasKey("ItemId", "PeopleId"); - b.HasIndex("Name"); + b.HasIndex("PeopleId"); b.HasIndex("ItemId", "ListOrder"); - b.ToTable("Peoples"); + b.HasIndex("ItemId", "SortOrder"); + + b.ToTable("PeopleBaseItemMap"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => @@ -1473,7 +1491,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.Navigation("Item"); }); - modelBuilder.Entity("Jellyfin.Data.Entities.People", b => + modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => { b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") .WithMany("Peoples") @@ -1481,7 +1499,15 @@ namespace Jellyfin.Server.Implementations.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("Jellyfin.Data.Entities.People", "People") + .WithMany("BaseItems") + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + b.Navigation("Item"); + + b.Navigation("People"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => @@ -1559,6 +1585,11 @@ namespace Jellyfin.Server.Implementations.Migrations b.Navigation("BaseItemsMap"); }); + modelBuilder.Entity("Jellyfin.Data.Entities.People", b => + { + b.Navigation("BaseItems"); + }); + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => { b.Navigation("AccessSchedules"); diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs new file mode 100644 index 0000000000..cdaee9161c --- /dev/null +++ b/Jellyfin.Server.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs @@ -0,0 +1,22 @@ +using System; +using Jellyfin.Data.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Jellyfin.Server.Implementations.ModelConfiguration; + +/// +/// People configuration. +/// +public class PeopleBaseItemMapConfiguration : IEntityTypeConfiguration +{ + /// + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(e => new { e.ItemId, e.PeopleId }); + builder.HasIndex(e => new { e.ItemId, e.SortOrder }); + builder.HasIndex(e => new { e.ItemId, e.ListOrder }); + builder.HasOne(e => e.Item); + builder.HasOne(e => e.People); + } +} diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/PeopleConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/PeopleConfiguration.cs index 5f5b4dfc74..f3cccb13fe 100644 --- a/Jellyfin.Server.Implementations/ModelConfiguration/PeopleConfiguration.cs +++ b/Jellyfin.Server.Implementations/ModelConfiguration/PeopleConfiguration.cs @@ -13,8 +13,8 @@ public class PeopleConfiguration : IEntityTypeConfiguration /// public void Configure(EntityTypeBuilder builder) { - builder.HasKey(e => new { e.ItemId, e.Role, e.ListOrder }); - builder.HasIndex(e => new { e.ItemId, e.ListOrder }); + builder.HasKey(e => e.Id); builder.HasIndex(e => e.Name); + builder.HasMany(e => e.BaseItems); } } diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index 9d4441ac39..0459436b15 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -47,7 +47,8 @@ namespace Jellyfin.Server.Migrations typeof(Routines.AddDefaultCastReceivers), typeof(Routines.UpdateDefaultPluginRepository), typeof(Routines.FixAudioData), - typeof(Routines.MoveTrickplayFiles) + typeof(Routines.MoveTrickplayFiles), + typeof(Routines.MigrateLibraryDb), }; /// diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index bad99c92f6..c88a609b67 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -1,26 +1,25 @@ using System; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; +using System.Data; +using System.Diagnostics; 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; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Chapter = Jellyfin.Data.Entities.Chapter; namespace Jellyfin.Server.Migrations.Routines; +#pragma warning disable RS0030 // Do not use banned APIs /// /// The migration routine for migrating the userdata database to EF Core. @@ -50,13 +49,13 @@ public class MigrateLibraryDb : IMigrationRoutine } /// - public Guid Id => Guid.Parse("5bcb4197-e7c0-45aa-9902-963bceab5798"); + public Guid Id => Guid.Parse("36445464-849f-429f-9ad0-bb130efa0664"); /// - public string Name => "MigrateUserData"; + public string Name => "MigrateLibraryDbData"; /// - public bool PerformOnNewInstall => false; + public bool PerformOnNewInstall => false; // TODO Change back after testing /// public void Perform() @@ -66,24 +65,38 @@ public class MigrateLibraryDb : IMigrationRoutine var dataPath = _paths.DataPath; var libraryDbPath = Path.Combine(dataPath, DbFilename); using var connection = new SqliteConnection($"Filename={libraryDbPath}"); + var stopwatch = new Stopwatch(); + stopwatch.Start(); connection.Open(); using var dbContext = _provider.CreateDbContext(); + _logger.LogInformation("Start moving UserData."); var queryResult = connection.Query("SELECT key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex FROM UserDatas"); dbContext.UserData.ExecuteDelete(); var users = dbContext.Users.AsNoTracking().ToImmutableArray(); - foreach (SqliteDataReader dto in queryResult) + foreach (var entity in queryResult) { - dbContext.UserData.Add(GetUserData(users, dto)); + var userData = GetUserData(users, entity); + if (userData is null) + { + _logger.LogError("Was not able to migrate user data with key {0}", entity.GetString(0)); + continue; + } + + dbContext.UserData.Add(userData); } + _logger.LogInformation("Try saving {0} UserData entries.", dbContext.UserData.Local.Count); dbContext.SaveChanges(); + var stepElapsed = stopwatch.Elapsed; + _logger.LogInformation("Saving UserData entries took {0}.", stepElapsed); - var typedBaseItemsQuery = "SELECT type, data, StartDate, EndDate, ChannelId, IsMovie, IsSeries, EpisodeTitle, IsRepeat, CommunityRating, CustomRating, IndexNumber, IsLocked, PreferredMetadataLanguage, PreferredMetadataCountryCode, Width, Height, DateLastRefreshed, Name, Path, PremiereDate, Overview, ParentIndexNumber, ProductionYear, OfficialRating, ForcedSortName, RunTimeTicks, Size, DateCreated, DateModified, guid, Genres, ParentId, Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId, DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, SeasonName, SeasonId, SeriesId, PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate, ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId FROM TypeBaseItems"; + _logger.LogInformation("Start moving TypedBaseItem."); + var typedBaseItemsQuery = "SELECT type, data, StartDate, EndDate, ChannelId, IsMovie, IsSeries, EpisodeTitle, IsRepeat, CommunityRating, CustomRating, IndexNumber, IsLocked, PreferredMetadataLanguage, PreferredMetadataCountryCode, Width, Height, DateLastRefreshed, Name, Path, PremiereDate, Overview, ParentIndexNumber, ProductionYear, OfficialRating, ForcedSortName, RunTimeTicks, Size, DateCreated, DateModified, guid, Genres, ParentId, Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId, DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, SeasonName, SeasonId, SeriesId, PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate, ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId FROM TypedBaseItems"; dbContext.BaseItems.ExecuteDelete(); foreach (SqliteDataReader dto in connection.Query(typedBaseItemsQuery)) @@ -91,9 +104,13 @@ public class MigrateLibraryDb : IMigrationRoutine dbContext.BaseItems.Add(GetItem(dto)); } + _logger.LogInformation("Try saving {0} BaseItem entries.", dbContext.BaseItems.Local.Count); dbContext.SaveChanges(); + stepElapsed = stopwatch.Elapsed - stepElapsed; + _logger.LogInformation("Saving BaseItems entries took {0}.", stepElapsed); - var mediaStreamQuery = "SELECT ItemId, StreamIndex, StreamType, Codec, Language, ChannelLayout, Profile, AspectRatio, Path, IsInterlaced, BitRate, Channels, SampleRate, IsDefault, IsForced, IsExternal, Height, Width, AverageFrameRate, RealFrameRate, Level, PixelFormat, BitDepth, IsAnamorphic, RefFrames, CodecTag, Comment, NalLengthSize, IsAvc, Title, TimeBase, CodecTimeBase, ColorPrimaries, ColorSpace, ColorTransfer, DvVersionMajor, DvVersionMinor, DvProfile, DvLevel, RpuPresentFlag, ElPresentFlag, BlPresentFlag, DvBlSignalCompatibilityId, IsHearingImpaired, Rotation FROM MediaStreams"; + _logger.LogInformation("Start moving MediaStreamInfos."); + var mediaStreamQuery = "SELECT ItemId, StreamIndex, StreamType, Codec, Language, ChannelLayout, Profile, AspectRatio, Path, IsInterlaced, BitRate, Channels, SampleRate, IsDefault, IsForced, IsExternal, Height, Width, AverageFrameRate, RealFrameRate, Level, PixelFormat, BitDepth, IsAnamorphic, RefFrames, CodecTag, Comment, NalLengthSize, IsAvc, Title, TimeBase, CodecTimeBase, ColorPrimaries, ColorSpace, ColorTransfer, DvVersionMajor, DvVersionMinor, DvProfile, DvLevel, RpuPresentFlag, ElPresentFlag, BlPresentFlag, DvBlSignalCompatibilityId, IsHearingImpaired FROM MediaStreams"; dbContext.MediaStreamInfos.ExecuteDelete(); foreach (SqliteDataReader dto in connection.Query(mediaStreamQuery)) @@ -101,18 +118,59 @@ public class MigrateLibraryDb : IMigrationRoutine dbContext.MediaStreamInfos.Add(GetMediaStream(dto)); } + _logger.LogInformation("Try saving {0} MediaStreamInfos entries.", dbContext.MediaStreamInfos.Local.Count); dbContext.SaveChanges(); + stepElapsed = stopwatch.Elapsed - stepElapsed; + _logger.LogInformation("Saving MediaStreamInfos entries took {0}.", stepElapsed); + _logger.LogInformation("Start moving People."); var personsQuery = "select ItemId, Name, Role, PersonType, SortOrder from People p"; dbContext.Peoples.ExecuteDelete(); + dbContext.PeopleBaseItemMap.ExecuteDelete(); - foreach (SqliteDataReader dto in connection.Query(personsQuery)) + foreach (SqliteDataReader reader in connection.Query(personsQuery)) { - dbContext.Peoples.Add(GetPerson(dto)); + var itemId = reader.GetGuid(0); + if (!dbContext.BaseItems.Any(f => f.Id == itemId)) + { + _logger.LogError("Dont save person {0} because its not in use by any BaseItem", reader.GetString(1)); + continue; + } + + var entity = GetPerson(reader); + var existingPerson = dbContext.Peoples.FirstOrDefault(e => e.Name == entity.Name); + if (existingPerson is null) + { + dbContext.Peoples.Add(entity); + existingPerson = entity; + } + + if (reader.TryGetString(2, out var role)) + { + } + + if (reader.TryGetInt32(4, out var sortOrder)) + { + } + + dbContext.PeopleBaseItemMap.Add(new PeopleBaseItemMap() + { + Item = null!, + ItemId = itemId, + People = existingPerson, + PeopleId = existingPerson.Id, + ListOrder = sortOrder, + SortOrder = sortOrder, + Role = role + }); } + _logger.LogInformation("Try saving {0} People entries.", dbContext.MediaStreamInfos.Local.Count); dbContext.SaveChanges(); + stepElapsed = stopwatch.Elapsed - stepElapsed; + _logger.LogInformation("Saving People entries took {0}.", stepElapsed); + _logger.LogInformation("Start moving ItemValues."); // do not migrate inherited types as they are now properly mapped in search and lookup. var itemValueQuery = "select ItemId, Type, Value, CleanValue FROM ItemValues WHERE Type <> 6"; dbContext.ItemValues.ExecuteDelete(); @@ -140,32 +198,60 @@ public class MigrateLibraryDb : IMigrationRoutine }); } + _logger.LogInformation("Try saving {0} ItemValues entries.", dbContext.ItemValues.Local.Count); dbContext.SaveChanges(); + stepElapsed = stopwatch.Elapsed - stepElapsed; + _logger.LogInformation("Saving People ItemValues took {0}.", stepElapsed); - var chapterQuery = "select StartPositionTicks,Name,ImagePath,ImageDateModified from Chapters2"; + _logger.LogInformation("Start moving Chapters."); + var chapterQuery = "select ItemId,StartPositionTicks,Name,ImagePath,ImageDateModified,ChapterIndex from Chapters2"; dbContext.Chapters.ExecuteDelete(); foreach (SqliteDataReader dto in connection.Query(chapterQuery)) { - dbContext.Chapters.Add(GetChapter(dto)); + var chapter = GetChapter(dto); + dbContext.Chapters.Add(chapter); } + _logger.LogInformation("Try saving {0} Chapters entries.", dbContext.Chapters.Local.Count); dbContext.SaveChanges(); + stepElapsed = stopwatch.Elapsed - stepElapsed; + _logger.LogInformation("Saving Chapters took {0}.", stepElapsed); + _logger.LogInformation("Start moving AncestorIds."); var ancestorIdsQuery = "select ItemId, AncestorId, AncestorIdText from AncestorIds"; dbContext.Chapters.ExecuteDelete(); foreach (SqliteDataReader dto in connection.Query(ancestorIdsQuery)) { - dbContext.AncestorIds.Add(GetAncestorId(dto)); + var ancestorId = GetAncestorId(dto); + if (!dbContext.BaseItems.Any(e => e.Id == ancestorId.ItemId)) + { + _logger.LogInformation("Dont move AncestorId ({0}, {1}) because no Item found.", ancestorId.ItemId, ancestorId.ParentItemId); + continue; + } + + if (!dbContext.BaseItems.Any(e => e.Id == ancestorId.ParentItemId)) + { + _logger.LogInformation("Dont move AncestorId ({0}, {1}) because no parent Item found.", ancestorId.ItemId, ancestorId.ParentItemId); + continue; + } + + dbContext.AncestorIds.Add(ancestorId); } + _logger.LogInformation("Try saving {0} AncestorIds entries.", dbContext.Chapters.Local.Count); + dbContext.SaveChanges(); + stepElapsed = stopwatch.Elapsed - stepElapsed; + _logger.LogInformation("Saving AncestorIds took {0}.", stepElapsed); connection.Close(); - _logger.LogInformation("Migration of the Library.db done."); - _logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old"); - File.Move(libraryDbPath, libraryDbPath + ".old"); + // _logger.LogInformation("Migration of the Library.db done."); + // _logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old"); + // File.Move(libraryDbPath, libraryDbPath + ".old"); + + _logger.LogInformation("Migrating Library db took {0}.", stopwatch.Elapsed); if (dbContext.Database.IsSqlite()) { @@ -180,12 +266,18 @@ public class MigrateLibraryDb : IMigrationRoutine } } - private static UserData GetUserData(ImmutableArray users, SqliteDataReader dto) + private static UserData? GetUserData(ImmutableArray users, SqliteDataReader dto) { + var indexOfUser = dto.GetInt32(1); + if (users.Length < indexOfUser) + { + return null; + } + return new UserData() { Key = dto.GetString(0), - UserId = users.ElementAt(dto.GetInt32(1)).Id, + UserId = users.ElementAt(indexOfUser).Id, Rating = dto.IsDBNull(2) ? null : dto.GetDouble(2), Played = dto.GetBoolean(3), PlayCount = dto.GetInt32(4), @@ -219,23 +311,23 @@ public class MigrateLibraryDb : IMigrationRoutine { var chapter = new Chapter { - StartPositionTicks = reader.GetInt64(0), - ChapterIndex = 0, + StartPositionTicks = reader.GetInt64(1), + ChapterIndex = reader.GetInt32(5), Item = null!, - ItemId = Guid.Empty + ItemId = reader.GetGuid(0), }; - if (reader.TryGetString(1, out var chapterName)) + if (reader.TryGetString(2, out var chapterName)) { chapter.Name = chapterName; } - if (reader.TryGetString(2, out var imagePath)) + if (reader.TryGetString(3, out var imagePath)) { chapter.ImagePath = imagePath; } - if (reader.TryReadDateTime(3, out var imageDateModified)) + if (reader.TryReadDateTime(4, out var imageDateModified)) { chapter.ImageDateModified = imageDateModified; } @@ -258,26 +350,15 @@ public class MigrateLibraryDb : IMigrationRoutine { var item = new People { - ItemId = reader.GetGuid(0), + Id = Guid.NewGuid(), Name = reader.GetString(1), - Item = null! }; - if (reader.TryGetString(2, out var role)) - { - item.Role = role; - } - if (reader.TryGetString(3, out var type)) { item.PersonType = type; } - if (reader.TryGetInt32(4, out var sortOrder)) - { - item.SortOrder = sortOrder; - } - return item; } @@ -515,10 +596,10 @@ public class MigrateLibraryDb : IMigrationRoutine item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result; - if (reader.TryGetInt32(44, out var rotation)) - { - item.Rotation = rotation; - } + // if (reader.TryGetInt32(44, out var rotation)) + // { + // item.Rotation = rotation; + // } return item; } diff --git a/MediaBrowser.Controller/Entities/PersonInfo.cs b/MediaBrowser.Controller/Entities/PersonInfo.cs index 3df0b0b785..0ed870bacf 100644 --- a/MediaBrowser.Controller/Entities/PersonInfo.cs +++ b/MediaBrowser.Controller/Entities/PersonInfo.cs @@ -17,8 +17,14 @@ namespace MediaBrowser.Controller.Entities public PersonInfo() { ProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); + Id = Guid.NewGuid(); } + /// + /// Gets or Sets the PersonId. + /// + public Guid Id { get; set; } + public Guid ItemId { get; set; } /// -- cgit v1.2.3 From 76df4c48bcc3f56d8a786fc349aa09543f7e2d9b Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 28 Oct 2024 11:54:39 +0000 Subject: Changed from ImmuntableList to ImmutableArray --- MediaBrowser.Controller/Entities/Movies/BoxSet.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index 4cddc91252..cb17e3fafd 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -131,13 +131,13 @@ namespace MediaBrowser.Controller.Entities.Movies public override IReadOnlyList GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) { var children = base.GetChildren(user, includeLinkedChildren, query); - return Sort(children, user).ToImmutableList(); + return Sort(children, user).ToImmutableArray(); } public override IReadOnlyList GetRecursiveChildren(User user, InternalItemsQuery query) { var children = base.GetRecursiveChildren(user, query); - return Sort(children, user).ToImmutableList(); + return Sort(children, user).ToImmutableArray(); } public BoxSetInfo GetLookupInfo() -- cgit v1.2.3 From 0639758abd157330c17bdc1831020bfbf6c0ce73 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 28 Oct 2024 14:34:29 +0000 Subject: Updated all instances of ImmutableList to ImmutableArray --- Emby.Server.Implementations/Library/MediaSourceManager.cs | 2 +- Jellyfin.Api/Controllers/YearsController.cs | 4 ++-- .../MediaSegments/MediaSegmentManager.cs | 2 +- MediaBrowser.Controller/Entities/BaseItem.cs | 2 +- MediaBrowser.Controller/Entities/Folder.cs | 8 ++++---- 5 files changed, 9 insertions(+), 9 deletions(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 3bf1a4cde9..2fb571a106 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -218,7 +218,7 @@ namespace Emby.Server.Implementations.Library list.Add(source); } - return SortMediaSources(list).ToImmutableList(); + return SortMediaSources(list).ToImmutableArray(); } /// > diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index ffc34a5d97..907724e040 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -113,11 +113,11 @@ public class YearsController : BaseJellyfinApiController if (userId.IsNullOrEmpty()) { - items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToImmutableList(); + items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToImmutableArray(); } else { - items = recursive ? folder.GetRecursiveChildren(user, query) : folder.GetChildren(user, true).Where(Filter).ToImmutableList(); + items = recursive ? folder.GetRecursiveChildren(user, query) : folder.GetChildren(user, true).Where(Filter).ToImmutableArray(); } } else diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs index d641f521b9..151b616f7e 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs @@ -154,7 +154,7 @@ public class MediaSegmentManager : IMediaSegmentManager return query .OrderBy(e => e.StartTicks) .AsNoTracking() - .ToImmutableList() + .ToImmutableArray() .Select(Map); } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 054c71db7e..58fae17717 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1092,7 +1092,7 @@ namespace MediaBrowser.Controller.Entities return 1; }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0) .ThenByDescending(i => i, new MediaSourceWidthComparator()) - .ToImmutableList(); + .ToImmutableArray(); } protected virtual IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources() diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 1bec66f952..8fff7dbc4d 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1306,7 +1306,7 @@ namespace MediaBrowser.Controller.Entities AddChildren(user, includeLinkedChildren, result, false, query); - return result.Values.ToImmutableList(); + return result.Values.ToImmutableArray(); } protected virtual IEnumerable GetEligibleChildrenForRecursiveChildren(User user) @@ -1379,7 +1379,7 @@ namespace MediaBrowser.Controller.Entities AddChildren(user, true, result, true, query); - return result.Values.ToImmutableList(); + return result.Values.ToImmutableArray(); } /// @@ -1407,7 +1407,7 @@ namespace MediaBrowser.Controller.Entities AddChildrenToList(result, includeLinkedChildren, true, filter); - return result.Values.ToImmutableList(); + return result.Values.ToImmutableArray(); } /// @@ -1563,7 +1563,7 @@ namespace MediaBrowser.Controller.Entities return LinkedChildren .Select(i => new Tuple(i, GetLinkedChild(i))) .Where(i => i.Item2 is not null) - .ToImmutableList(); + .ToImmutableArray(); } protected override async Task RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList fileSystemChildren, CancellationToken cancellationToken) -- cgit v1.2.3 From d2db7004024c6bbdd541a381c673f1e0b0aebfcb Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Thu, 31 Oct 2024 17:02:06 +0100 Subject: Always await instead of directly returning Task https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md#prefer-asyncawait-over-directly-returning-task The performance impact is negligible (and it's me saying that!) --- .../HttpServer/WebSocketConnection.cs | 14 ++++++------- .../HttpServer/WebSocketManager.cs | 4 ++-- .../Session/SessionWebSocketListener.cs | 6 +++--- Jellyfin.Api/Controllers/DynamicHlsController.cs | 6 +++--- Jellyfin.Api/Formatters/CssOutputFormatter.cs | 24 ++++------------------ Jellyfin.Api/Formatters/XmlOutputFormatter.cs | 15 +------------- .../SymlinkFollowingPhysicalFileResultExecutor.cs | 16 ++++++++------- .../Entities/Audio/MusicArtist.cs | 6 +++--- MediaBrowser.Controller/Entities/BaseItem.cs | 20 ++++++++---------- MediaBrowser.Controller/Entities/Folder.cs | 12 +++++------ .../Savers/PlaylistXmlSaver.cs | 6 +++--- .../Plugins/AudioDb/AudioDbAlbumProvider.cs | 12 +++++------ .../Plugins/AudioDb/AudioDbArtistProvider.cs | 6 +++--- .../Plugins/StudioImages/StudiosImageProvider.cs | 4 ++-- 14 files changed, 60 insertions(+), 91 deletions(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index cb6f7e1d35..a720c86fb2 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -82,17 +82,17 @@ namespace Emby.Server.Implementations.HttpServer public WebSocketState State => _socket.State; /// - public Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken) + public async Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken) { var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions); - return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken); + await _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false); } /// - public Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken) + public async Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken) { var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions); - return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken); + await _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false); } /// @@ -224,12 +224,12 @@ namespace Emby.Server.Implementations.HttpServer return ret; } - private Task SendKeepAliveResponse() + private async Task SendKeepAliveResponse() { LastKeepAliveDate = DateTime.UtcNow; - return SendAsync( + await SendAsync( new OutboundKeepAliveMessage(), - CancellationToken.None); + CancellationToken.None).ConfigureAwait(false); } /// diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs index 774d3563cb..cb5b3993b8 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs @@ -84,7 +84,7 @@ namespace Emby.Server.Implementations.HttpServer /// Processes the web socket message received. /// /// The result. - private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result) + private async Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result) { var tasks = new Task[_webSocketListeners.Length]; for (var i = 0; i < _webSocketListeners.Length; ++i) @@ -92,7 +92,7 @@ namespace Emby.Server.Implementations.HttpServer tasks[i] = _webSocketListeners[i].ProcessMessageAsync(result); } - return Task.WhenAll(tasks); + await Task.WhenAll(tasks).ConfigureAwait(false); } } } diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index aba51de8f5..75a50f7f52 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -276,11 +276,11 @@ namespace Emby.Server.Implementations.Session /// /// The WebSocket. /// Task. - private Task SendForceKeepAlive(IWebSocketConnection webSocket) + private async Task SendForceKeepAlive(IWebSocketConnection webSocket) { - return webSocket.SendAsync( + await webSocket.SendAsync( new ForceKeepAliveMessage(WebSocketLostTimeout), - CancellationToken.None); + CancellationToken.None).ConfigureAwait(false); } } } diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 54e0527c90..9485927126 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -2059,16 +2059,16 @@ public class DynamicHlsController : BaseJellyfinApiController } } - private Task DeleteLastFile(string playlistPath, string segmentExtension, int retryCount) + private async Task DeleteLastFile(string playlistPath, string segmentExtension, int retryCount) { var file = GetLastTranscodingFile(playlistPath, segmentExtension, _fileSystem); if (file is null) { - return Task.CompletedTask; + return; } - return DeleteFile(file.FullName, retryCount); + await DeleteFile(file.FullName, retryCount).ConfigureAwait(false); } private async Task DeleteFile(string path, int retryCount) diff --git a/Jellyfin.Api/Formatters/CssOutputFormatter.cs b/Jellyfin.Api/Formatters/CssOutputFormatter.cs index 495f771e1f..9ad1c863ea 100644 --- a/Jellyfin.Api/Formatters/CssOutputFormatter.cs +++ b/Jellyfin.Api/Formatters/CssOutputFormatter.cs @@ -1,6 +1,4 @@ -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; +using System.Net.Mime; using Microsoft.AspNetCore.Mvc.Formatters; namespace Jellyfin.Api.Formatters; @@ -8,28 +6,14 @@ namespace Jellyfin.Api.Formatters; /// /// Css output formatter. /// -public class CssOutputFormatter : TextOutputFormatter +public sealed class CssOutputFormatter : StringOutputFormatter { /// /// Initializes a new instance of the class. /// public CssOutputFormatter() { - SupportedMediaTypes.Add("text/css"); - - SupportedEncodings.Add(Encoding.UTF8); - SupportedEncodings.Add(Encoding.Unicode); - } - - /// - /// Write context object to stream. - /// - /// Writer context. - /// Unused. Writer encoding. - /// Write stream task. - public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) - { - var stringResponse = context.Object?.ToString(); - return stringResponse is null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse); + SupportedMediaTypes.Clear(); + SupportedMediaTypes.Add(MediaTypeNames.Text.Css); } } diff --git a/Jellyfin.Api/Formatters/XmlOutputFormatter.cs b/Jellyfin.Api/Formatters/XmlOutputFormatter.cs index 1c9feedcb7..8dbb91d0aa 100644 --- a/Jellyfin.Api/Formatters/XmlOutputFormatter.cs +++ b/Jellyfin.Api/Formatters/XmlOutputFormatter.cs @@ -1,7 +1,4 @@ using System.Net.Mime; -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Formatters; namespace Jellyfin.Api.Formatters; @@ -9,7 +6,7 @@ namespace Jellyfin.Api.Formatters; /// /// Xml output formatter. /// -public class XmlOutputFormatter : TextOutputFormatter +public sealed class XmlOutputFormatter : StringOutputFormatter { /// /// Initializes a new instance of the class. @@ -18,15 +15,5 @@ public class XmlOutputFormatter : TextOutputFormatter { SupportedMediaTypes.Clear(); SupportedMediaTypes.Add(MediaTypeNames.Text.Xml); - - SupportedEncodings.Add(Encoding.UTF8); - SupportedEncodings.Add(Encoding.Unicode); - } - - /// - public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) - { - var stringResponse = context.Object?.ToString(); - return stringResponse is null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse); } } diff --git a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs index 801026c549..999e083845 100644 --- a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs +++ b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs @@ -67,38 +67,40 @@ namespace Jellyfin.Server.Infrastructure } /// - protected override Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue? range, long rangeLength) + protected override async Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue? range, long rangeLength) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(result); if (range is not null && rangeLength == 0) { - return Task.CompletedTask; + return; } // It's a bit of wasted IO to perform this check again, but non-symlinks shouldn't use this code if (!IsSymLink(result.FileName)) { - return base.WriteFileAsync(context, result, range, rangeLength); + await base.WriteFileAsync(context, result, range, rangeLength).ConfigureAwait(false); + return; } var response = context.HttpContext.Response; if (range is not null) { - return SendFileAsync( + await SendFileAsync( result.FileName, response, offset: range.From ?? 0L, - count: rangeLength); + count: rangeLength).ConfigureAwait(false); + return; } - return SendFileAsync( + await SendFileAsync( result.FileName, response, offset: 0, - count: null); + count: null).ConfigureAwait(false); } private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count) diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 1ab6c97066..5b7b03de21 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -110,15 +110,15 @@ namespace MediaBrowser.Controller.Entities.Audio return base.IsSaveLocalMetadataEnabled(); } - protected override Task ValidateChildrenInternal(IProgress progress, bool recursive, bool refreshChildMetadata, bool allowRemoveRoot, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) + protected override async Task ValidateChildrenInternal(IProgress progress, bool recursive, bool refreshChildMetadata, bool allowRemoveRoot, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { if (IsAccessedByName) { // Should never get in here anyway - return Task.CompletedTask; + return; } - return base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, false, refreshOptions, directoryService, cancellationToken); + await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, false, refreshOptions, directoryService, cancellationToken).ConfigureAwait(false); } public override List GetUserDataKeys() diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index eb605f6c87..a9caa8453a 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1977,8 +1977,8 @@ namespace MediaBrowser.Controller.Entities ImageInfos = [..ImageInfos, image]; } - public virtual Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken) - => LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken); + public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken) + => await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(false); /// /// Validates that images within the item are still on the filesystem. @@ -2367,7 +2367,7 @@ namespace MediaBrowser.Controller.Entities } } - protected Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOptions options, CancellationToken cancellationToken) + protected async Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOptions options, CancellationToken cancellationToken) { var newOptions = new MetadataRefreshOptions(options) { @@ -2428,10 +2428,10 @@ namespace MediaBrowser.Controller.Entities } } - return ownedItem.RefreshMetadata(newOptions, cancellationToken); + await ownedItem.RefreshMetadata(newOptions, cancellationToken).ConfigureAwait(false); } - protected Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken) + protected async Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken) { var newOptions = new MetadataRefreshOptions(options) { @@ -2441,9 +2441,7 @@ namespace MediaBrowser.Controller.Entities var id = LibraryManager.GetNewItemId(path, typeof(Video)); // Try to retrieve it from the db. If we don't find it, use the resolved version - var video = LibraryManager.GetItemById(id) as Video; - - if (video is null) + if (LibraryManager.GetItemById(id) is not Video video) { video = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Video; @@ -2452,15 +2450,15 @@ namespace MediaBrowser.Controller.Entities if (video is null) { - return Task.FromResult(true); + return; } if (video.OwnerId.IsEmpty()) { - video.OwnerId = this.Id; + video.OwnerId = Id; } - return RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken); + await RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken).ConfigureAwait(false); } public string GetEtag(User user) diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 83c19a54e1..41e26fc9ae 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -528,13 +528,13 @@ namespace MediaBrowser.Controller.Entities } } - private Task RefreshMetadataRecursive(IList children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress progress, CancellationToken cancellationToken) + private async Task RefreshMetadataRecursive(IList children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress progress, CancellationToken cancellationToken) { - return RunTasks( + await RunTasks( (baseItem, innerProgress) => RefreshChildMetadata(baseItem, refreshOptions, recursive && baseItem.IsFolder, innerProgress, cancellationToken), children, progress, - cancellationToken); + cancellationToken).ConfigureAwait(false); } private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOptions, IProgress progress, CancellationToken cancellationToken) @@ -575,13 +575,13 @@ namespace MediaBrowser.Controller.Entities /// The progress. /// The cancellation token. /// Task. - private Task ValidateSubFolders(IList children, IDirectoryService directoryService, IProgress progress, CancellationToken cancellationToken) + private async Task ValidateSubFolders(IList children, IDirectoryService directoryService, IProgress progress, CancellationToken cancellationToken) { - return RunTasks( + await RunTasks( (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, true, false, false, null, directoryService, cancellationToken), children, progress, - cancellationToken); + cancellationToken).ConfigureAwait(false); } /// diff --git a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs index 3f018cae9b..ae767a72ac 100644 --- a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs @@ -45,16 +45,16 @@ namespace MediaBrowser.LocalMetadata.Savers } /// - protected override Task WriteCustomElementsAsync(BaseItem item, XmlWriter writer) + protected override async Task WriteCustomElementsAsync(BaseItem item, XmlWriter writer) { var game = (Playlist)item; if (game.PlaylistMediaType == MediaType.Unknown) { - return Task.CompletedTask; + return; } - return writer.WriteElementStringAsync(null, "PlaylistMediaType", null, game.PlaylistMediaType.ToString()); + await writer.WriteElementStringAsync(null, "PlaylistMediaType", null, game.PlaylistMediaType.ToString()).ConfigureAwait(false); } /// diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs index daad9706cd..ff30af8797 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs @@ -148,21 +148,19 @@ namespace MediaBrowser.Providers.Plugins.AudioDb item.Overview = (overview ?? string.Empty).StripHtml(); } - internal Task EnsureInfo(string musicBrainzReleaseGroupId, CancellationToken cancellationToken) + internal async Task EnsureInfo(string musicBrainzReleaseGroupId, CancellationToken cancellationToken) { var xmlPath = GetAlbumInfoPath(_config.ApplicationPaths, musicBrainzReleaseGroupId); var fileInfo = _fileSystem.GetFileSystemInfo(xmlPath); - if (fileInfo.Exists) + if (fileInfo.Exists + && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) { - if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) - { - return Task.CompletedTask; - } + return; } - return DownloadInfo(musicBrainzReleaseGroupId, cancellationToken); + await DownloadInfo(musicBrainzReleaseGroupId, cancellationToken).ConfigureAwait(false); } internal async Task DownloadInfo(string musicBrainzReleaseGroupId, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs index 92742b1aa5..00bd96282c 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs @@ -131,7 +131,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb item.Overview = (overview ?? string.Empty).StripHtml(); } - internal Task EnsureArtistInfo(string musicBrainzId, CancellationToken cancellationToken) + internal async Task EnsureArtistInfo(string musicBrainzId, CancellationToken cancellationToken) { var xmlPath = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId); @@ -140,10 +140,10 @@ namespace MediaBrowser.Providers.Plugins.AudioDb if (fileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) { - return Task.CompletedTask; + return; } - return DownloadArtistInfo(musicBrainzId, cancellationToken); + await DownloadArtistInfo(musicBrainzId, cancellationToken).ConfigureAwait(false); } internal async Task DownloadArtistInfo(string musicBrainzId, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs index 5ca9f6f9a4..a50d69df53 100644 --- a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs @@ -101,11 +101,11 @@ namespace MediaBrowser.Providers.Plugins.StudioImages return string.Format(CultureInfo.InvariantCulture, "{0}/images/{1}/{2}.jpg", GetRepositoryUrl(), image, filename); } - private Task EnsureThumbsList(string file, CancellationToken cancellationToken) + private async Task EnsureThumbsList(string file, CancellationToken cancellationToken) { string url = string.Format(CultureInfo.InvariantCulture, "{0}/thumbs.txt", GetRepositoryUrl()); - return EnsureList(url, file, _fileSystem, cancellationToken); + await EnsureList(url, file, _fileSystem, cancellationToken).ConfigureAwait(false); } /// -- cgit v1.2.3 From 4959232b271ca83b6a38571f7cbb7a1ce112ab2f Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sun, 10 Nov 2024 19:28:41 +0000 Subject: Fixed tags aggregation --- MediaBrowser.Controller/Entities/BaseItem.cs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 58fae17717..7b279fa697 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1588,16 +1588,26 @@ namespace MediaBrowser.Controller.Entities public List GetInheritedTags() { var list = new List(); - list.AddRange(Tags); + if (Tags is not null) + { + list.AddRange(Tags); + } foreach (var parent in GetParents()) { - list.AddRange(parent.Tags); + if (parent.Tags is not null) + { + list.AddRange(parent.Tags); + } } foreach (var folder in LibraryManager.GetCollectionFolders(this)) { - list.AddRange(folder.Tags); + if (folder.Tags is not null) + { + list.AddRange(folder.Tags); + } + } return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); @@ -1785,7 +1795,7 @@ namespace MediaBrowser.Controller.Entities } else { - Studios = [..current, name]; + Studios = [.. current, name]; } } } @@ -1807,7 +1817,7 @@ namespace MediaBrowser.Controller.Entities var genres = Genres; if (!genres.Contains(name, StringComparison.OrdinalIgnoreCase)) { - Genres = [..genres, name]; + Genres = [.. genres, name]; } } @@ -1978,7 +1988,7 @@ namespace MediaBrowser.Controller.Entities public void AddImage(ItemImageInfo image) { - ImageInfos = [..ImageInfos, image]; + ImageInfos = [.. ImageInfos, image]; } public virtual Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken) -- cgit v1.2.3 From dfbbbf023d7dbbb7bedb9cd8c30cb8acb584a00f Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sun, 10 Nov 2024 20:10:59 +0000 Subject: reverted tag enumeration --- MediaBrowser.Controller/Entities/BaseItem.cs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 7b279fa697..0c698bb94f 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1588,26 +1588,16 @@ namespace MediaBrowser.Controller.Entities public List GetInheritedTags() { var list = new List(); - if (Tags is not null) - { - list.AddRange(Tags); - } + list.AddRange(Tags); foreach (var parent in GetParents()) { - if (parent.Tags is not null) - { - list.AddRange(parent.Tags); - } + list.AddRange(parent.Tags); } foreach (var folder in LibraryManager.GetCollectionFolders(this)) { - if (folder.Tags is not null) - { - list.AddRange(folder.Tags); - } - + list.AddRange(folder.Tags); } return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); -- cgit v1.2.3 From 508b27f15643dc04d0ca1dda92a3b18bdeb43a5a Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 11 Nov 2024 17:39:50 +0000 Subject: Fixed Duplicate returns on grouping Fixed UserDataKey not stored --- .../Library/UserDataManager.cs | 35 +- Jellyfin.Data/Entities/UserData.cs | 6 + .../Item/BaseItemRepository.cs | 53 +- .../20241111131257_AddedCustomDataKey.Designer.cs | 1610 ++++++++++++++++++++ .../20241111131257_AddedCustomDataKey.cs | 28 + ...0241111135439_AddedCustomDataKeyKey.Designer.cs | 1610 ++++++++++++++++++++ .../20241111135439_AddedCustomDataKeyKey.cs | 54 + .../Migrations/JellyfinDbModelSnapshot.cs | 5 +- .../ModelConfiguration/UserDataConfiguration.cs | 2 +- .../Migrations/Routines/MigrateLibraryDb.cs | 17 +- MediaBrowser.Controller/Entities/BaseItem.cs | 5 +- 11 files changed, 3391 insertions(+), 34 deletions(-) create mode 100644 Jellyfin.Server.Implementations/Migrations/20241111131257_AddedCustomDataKey.Designer.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20241111131257_AddedCustomDataKey.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20241111135439_AddedCustomDataKeyKey.cs (limited to 'MediaBrowser.Controller/Entities') diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index 371fc22c76..6974c0480d 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.Linq; using System.Threading; using Jellyfin.Data.Entities; +using Jellyfin.Extensions; using Jellyfin.Server.Implementations; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; @@ -65,7 +66,15 @@ namespace Emby.Server.Implementations.Library foreach (var key in keys) { userData.Key = key; - repository.UserData.Add(Map(userData, user.Id)); + var userDataEntry = Map(userData, user.Id, item.Id); + if (repository.UserData.Any(f => f.ItemId == item.Id && f.UserId == user.Id && f.CustomDataKey == key)) + { + repository.UserData.Attach(userDataEntry).State = EntityState.Modified; + } + else + { + repository.UserData.Add(userDataEntry); + } } repository.SaveChanges(); @@ -131,11 +140,12 @@ namespace Emby.Server.Implementations.Library SaveUserData(user, item, userData, reason, CancellationToken.None); } - private UserData Map(UserItemData dto, Guid userId) + private UserData Map(UserItemData dto, Guid userId, Guid itemId) { return new UserData() { - ItemId = Guid.Parse(dto.Key), + ItemId = itemId, + CustomDataKey = dto.Key, Item = null!, User = null!, AudioStreamIndex = dto.AudioStreamIndex, @@ -155,7 +165,7 @@ namespace Emby.Server.Implementations.Library { return new UserItemData() { - Key = dto.ItemId.ToString("D"), + Key = dto.CustomDataKey!, AudioStreamIndex = dto.AudioStreamIndex, IsFavorite = dto.IsFavorite, LastPlayedDate = dto.LastPlayedDate, @@ -175,7 +185,10 @@ namespace Emby.Server.Implementations.Library if (data is null) { - return null; + return new UserItemData() + { + Key = keys[0], + }; } return _userData.GetOrAdd(cacheKey, data); @@ -184,13 +197,9 @@ namespace Emby.Server.Implementations.Library private UserItemData? GetUserDataInternal(Guid userId, List keys) { var key = keys.FirstOrDefault(); - if (key is null || !Guid.TryParse(key, out var itemId)) - { - return null; - } using var context = _repository.CreateDbContext(); - var userData = context.UserData.AsNoTracking().FirstOrDefault(e => e.ItemId == itemId && e.UserId.Equals(userId)); + var userData = context.UserData.AsNoTracking().FirstOrDefault(e => e.CustomDataKey == key && e.UserId.Equals(userId)); if (userData is not null) { @@ -236,7 +245,7 @@ namespace Emby.Server.Implementations.Library return null; } - var dto = GetUserItemDataDto(userData); + var dto = GetUserItemDataDto(userData, item.Id); item.FillUserDataDtoValues(dto, userData, itemDto, user, options); return dto; @@ -246,9 +255,10 @@ namespace Emby.Server.Implementations.Library /// Converts a UserItemData to a DTOUserItemData. /// /// The data. + /// The the reference key to an Item. /// DtoUserItemData. /// is null. - private UserItemDataDto GetUserItemDataDto(UserItemData data) + private UserItemDataDto GetUserItemDataDto(UserItemData data, Guid itemId) { ArgumentNullException.ThrowIfNull(data); @@ -261,6 +271,7 @@ namespace Emby.Server.Implementations.Library Rating = data.Rating, Played = data.Played, LastPlayedDate = data.LastPlayedDate, + ItemId = itemId, Key = data.Key }; } diff --git a/Jellyfin.Data/Entities/UserData.cs b/Jellyfin.Data/Entities/UserData.cs index fe8c8c5cea..05ab6dd2d2 100644 --- a/Jellyfin.Data/Entities/UserData.cs +++ b/Jellyfin.Data/Entities/UserData.cs @@ -8,6 +8,12 @@ namespace Jellyfin.Data.Entities; /// public class UserData { + /// + /// Gets or sets the custom data key. + /// + /// The rating. + public required string CustomDataKey { get; set; } + /// /// Gets or sets the users 0-10 rating. /// diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 4af03abf1b..151b65089d 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -116,22 +116,23 @@ public sealed class BaseItemRepository( using var context = dbProvider.CreateDbContext(); var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter); - // .DistinctBy(e => e.Id); var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter); if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey) { - dbQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).SelectMany(e => e); + dbQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(e => e.First()); } - - if (enableGroupByPresentationUniqueKey) + else if (enableGroupByPresentationUniqueKey) { - dbQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).SelectMany(e => e); + dbQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.First()); } - - if (filter.GroupBySeriesPresentationUniqueKey) + else if (filter.GroupBySeriesPresentationUniqueKey) + { + dbQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.First()); + } + else { - dbQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).SelectMany(e => e); + dbQuery = dbQuery.Distinct(); } dbQuery = ApplyOrder(dbQuery, filter); @@ -225,9 +226,15 @@ public sealed class BaseItemRepository( IQueryable dbQuery = context.BaseItems.AsNoTracking() .Include(e => e.TrailerTypes) .Include(e => e.Provider) - .Include(e => e.Images) .Include(e => e.LockedFields); + + if (filter.DtoOptions.EnableImages) + { + dbQuery = dbQuery.Include(e => e.Images); + } + dbQuery = TranslateQuery(dbQuery, context, filter); + dbQuery = dbQuery.Distinct(); // .DistinctBy(e => e.Id); if (filter.EnableTotalRecordCount) { @@ -266,10 +273,34 @@ public sealed class BaseItemRepository( IQueryable dbQuery = context.BaseItems.AsNoTracking() .Include(e => e.TrailerTypes) .Include(e => e.Provider) - .Include(e => e.Images) .Include(e => e.LockedFields); + + if (filter.DtoOptions.EnableImages) + { + dbQuery = dbQuery.Include(e => e.Images); + } + dbQuery = TranslateQuery(dbQuery, context, filter); dbQuery = ApplyOrder(dbQuery, filter); + + var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter); + if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey) + { + dbQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(e => e.First()); + } + else if (enableGroupByPresentationUniqueKey) + { + dbQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.First()); + } + else if (filter.GroupBySeriesPresentationUniqueKey) + { + dbQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.First()); + } + else + { + dbQuery = dbQuery.Distinct(); + } + if (filter.Limit.HasValue || filter.StartIndex.HasValue) { var offset = filter.StartIndex ?? 0; @@ -1330,7 +1361,7 @@ public sealed class BaseItemRepository( .Include(e => e.TrailerTypes) .Include(e => e.Provider) .Include(e => e.Images) - .Include(e => e.LockedFields).AsNoTracking().FirstOrDefault(e => e.Id == id); + .Include(e => e.LockedFields).AsNoTracking().AsSingleQuery().FirstOrDefault(e => e.Id == id); if (item is null) { return null; diff --git a/Jellyfin.Server.Implementations/Migrations/20241111131257_AddedCustomDataKey.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20241111131257_AddedCustomDataKey.Designer.cs new file mode 100644 index 0000000000..1fbf21492d --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20241111131257_AddedCustomDataKey.Designer.cs @@ -0,0 +1,1610 @@ +// +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("20241111131257_AddedCustomDataKey")] + partial class AddedCustomDataKey + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ParentItemId") + .HasColumnType("TEXT"); + + b.Property("BaseItemEntityId") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ParentItemId"); + + b.HasIndex("BaseItemEntityId"); + + b.HasIndex("ParentItemId"); + + b.ToTable("AncestorIds"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Filename") + .HasColumnType("TEXT"); + + b.Property("MimeType") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "Index"); + + b.ToTable("AttachmentStreamInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("AlbumArtists") + .HasColumnType("TEXT"); + + b.Property("Artists") + .HasColumnType("TEXT"); + + b.Property("Audio") + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("TEXT"); + + b.Property("CleanName") + .HasColumnType("TEXT"); + + b.Property("CommunityRating") + .HasColumnType("REAL"); + + b.Property("CriticRating") + .HasColumnType("REAL"); + + b.Property("CustomRating") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastMediaAdded") + .HasColumnType("TEXT"); + + b.Property("DateLastRefreshed") + .HasColumnType("TEXT"); + + b.Property("DateLastSaved") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("EpisodeTitle") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasColumnType("TEXT"); + + b.Property("ExternalSeriesId") + .HasColumnType("TEXT"); + + b.Property("ExternalServiceId") + .HasColumnType("TEXT"); + + b.Property("ExtraIds") + .HasColumnType("TEXT"); + + b.Property("ExtraType") + .HasColumnType("INTEGER"); + + b.Property("ForcedSortName") + .HasColumnType("TEXT"); + + b.Property("Genres") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IndexNumber") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingValue") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("IsInMixedFolder") + .HasColumnType("INTEGER"); + + b.Property("IsLocked") + .HasColumnType("INTEGER"); + + b.Property("IsMovie") + .HasColumnType("INTEGER"); + + b.Property("IsRepeat") + .HasColumnType("INTEGER"); + + b.Property("IsSeries") + .HasColumnType("INTEGER"); + + b.Property("IsVirtualItem") + .HasColumnType("INTEGER"); + + b.Property("LUFS") + .HasColumnType("REAL"); + + b.Property("MediaType") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizationGain") + .HasColumnType("REAL"); + + b.Property("OfficialRating") + .HasColumnType("TEXT"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ParentIndexNumber") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataCountryCode") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataLanguage") + .HasColumnType("TEXT"); + + b.Property("PremiereDate") + .HasColumnType("TEXT"); + + b.Property("PresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("PrimaryVersionId") + .HasColumnType("TEXT"); + + b.Property("ProductionLocations") + .HasColumnType("TEXT"); + + b.Property("ProductionYear") + .HasColumnType("INTEGER"); + + b.Property("RunTimeTicks") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("TEXT"); + + b.Property("SeasonName") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("SeriesName") + .HasColumnType("TEXT"); + + b.Property("SeriesPresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("ShowId") + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Studios") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("TopParentId") + .HasColumnType("TEXT"); + + b.Property("TotalBitrate") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UnratedType") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("Path"); + + b.HasIndex("PresentationUniqueKey"); + + b.HasIndex("TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "PresentationUniqueKey"); + + b.HasIndex("Type", "TopParentId", "StartDate"); + + b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem"); + + b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName"); + + b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.ToTable("BaseItems"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Blurhash") + .HasColumnType("BLOB"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("ImageType") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemMetadataFields"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ProviderId"); + + b.HasIndex("ProviderId", "ProviderValue", "ItemId"); + + b.ToTable("BaseItemProviders"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemTrailerTypes"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ChapterIndex") + .HasColumnType("INTEGER"); + + b.Property("ImageDateModified") + .HasColumnType("TEXT"); + + b.Property("ImagePath") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("StartPositionTicks") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "ChapterIndex"); + + b.ToTable("Chapters"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => + { + b.Property("ItemValueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CleanValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId"); + + b.HasIndex("Type", "CleanValue"); + + b.ToTable("ItemValues"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => + { + b.Property("ItemValueId") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("ItemValuesMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EndTicks") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("SegmentProviderId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartTicks") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSegments"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("AspectRatio") + .HasColumnType("TEXT"); + + b.Property("AverageFrameRate") + .HasColumnType("REAL"); + + b.Property("BitDepth") + .HasColumnType("INTEGER"); + + b.Property("BitRate") + .HasColumnType("INTEGER"); + + b.Property("BlPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("ChannelLayout") + .HasColumnType("TEXT"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CodecTimeBase") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ColorPrimaries") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ColorSpace") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ColorTransfer") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DvBlSignalCompatibilityId") + .HasColumnType("INTEGER"); + + b.Property("DvLevel") + .HasColumnType("INTEGER"); + + b.Property("DvProfile") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMajor") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMinor") + .HasColumnType("INTEGER"); + + b.Property("ElPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IsAnamorphic") + .HasColumnType("INTEGER"); + + b.Property("IsAvc") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsExternal") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("IsHearingImpaired") + .HasColumnType("INTEGER"); + + b.Property("IsInterlaced") + .HasColumnType("INTEGER"); + + b.Property("KeyFrames") + .HasColumnType("TEXT"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("REAL"); + + b.Property("NalLengthSize") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PixelFormat") + .HasColumnType("TEXT"); + + b.Property("Profile") + .HasColumnType("TEXT"); + + b.Property("RealFrameRate") + .HasColumnType("REAL"); + + b.Property("RefFrames") + .HasColumnType("INTEGER"); + + b.Property("Rotation") + .HasColumnType("INTEGER"); + + b.Property("RpuPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("SampleRate") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .HasColumnType("INTEGER"); + + b.Property("TimeBase") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "StreamIndex"); + + b.HasIndex("StreamIndex"); + + b.HasIndex("StreamType"); + + b.HasIndex("StreamIndex", "StreamType"); + + b.HasIndex("StreamIndex", "StreamType", "Language"); + + b.ToTable("MediaStreamInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.People", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PersonType") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Peoples"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("PeopleId") + .HasColumnType("TEXT"); + + b.Property("ListOrder") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.HasIndex("ItemId", "ListOrder"); + + b.HasIndex("ItemId", "SortOrder"); + + b.ToTable("PeopleBaseItemMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("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("ItemId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("AudioStreamIndex") + .HasColumnType("INTEGER"); + + b.Property("CustomDataKey") + .HasColumnType("TEXT"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER"); + + b.Property("LastPlayedDate") + .HasColumnType("TEXT"); + + b.Property("Likes") + .HasColumnType("INTEGER"); + + b.Property("PlayCount") + .HasColumnType("INTEGER"); + + b.Property("PlaybackPositionTicks") + .HasColumnType("INTEGER"); + + b.Property("Played") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("SubtitleStreamIndex") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "UserId"); + + b.HasIndex("UserId"); + + b.HasIndex("ItemId", "UserId", "IsFavorite"); + + b.HasIndex("ItemId", "UserId", "LastPlayedDate"); + + b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks"); + + b.HasIndex("ItemId", "UserId", "Played"); + + b.ToTable("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", null) + .WithMany("AncestorIds") + .HasForeignKey("BaseItemEntityId"); + + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem") + .WithMany() + .HasForeignKey("ParentItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ParentItem"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Images") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("LockedFields") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Provider") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("TrailerTypes") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Chapters") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("ItemValues") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue") + .WithMany("BaseItemsMap") + .HasForeignKey("ItemValueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ItemValue"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("MediaStreams") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Peoples") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.People", "People") + .WithMany("BaseItems") + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("People"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("UserData") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => + { + b.Navigation("AncestorIds"); + + b.Navigation("Chapters"); + + b.Navigation("Images"); + + b.Navigation("ItemValues"); + + b.Navigation("LockedFields"); + + b.Navigation("MediaStreams"); + + b.Navigation("Peoples"); + + b.Navigation("Provider"); + + b.Navigation("TrailerTypes"); + + b.Navigation("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => + { + b.Navigation("BaseItemsMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.People", b => + { + b.Navigation("BaseItems"); + }); + + 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/20241111131257_AddedCustomDataKey.cs b/Jellyfin.Server.Implementations/Migrations/20241111131257_AddedCustomDataKey.cs new file mode 100644 index 0000000000..ac78019eda --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20241111131257_AddedCustomDataKey.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// + public partial class AddedCustomDataKey : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CustomDataKey", + table: "UserData", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CustomDataKey", + table: "UserData"); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs new file mode 100644 index 0000000000..bac6fd5b5a --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs @@ -0,0 +1,1610 @@ +// +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("20241111135439_AddedCustomDataKeyKey")] + partial class AddedCustomDataKeyKey + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ParentItemId") + .HasColumnType("TEXT"); + + b.Property("BaseItemEntityId") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ParentItemId"); + + b.HasIndex("BaseItemEntityId"); + + b.HasIndex("ParentItemId"); + + b.ToTable("AncestorIds"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Filename") + .HasColumnType("TEXT"); + + b.Property("MimeType") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "Index"); + + b.ToTable("AttachmentStreamInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("AlbumArtists") + .HasColumnType("TEXT"); + + b.Property("Artists") + .HasColumnType("TEXT"); + + b.Property("Audio") + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("TEXT"); + + b.Property("CleanName") + .HasColumnType("TEXT"); + + b.Property("CommunityRating") + .HasColumnType("REAL"); + + b.Property("CriticRating") + .HasColumnType("REAL"); + + b.Property("CustomRating") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastMediaAdded") + .HasColumnType("TEXT"); + + b.Property("DateLastRefreshed") + .HasColumnType("TEXT"); + + b.Property("DateLastSaved") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("EpisodeTitle") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasColumnType("TEXT"); + + b.Property("ExternalSeriesId") + .HasColumnType("TEXT"); + + b.Property("ExternalServiceId") + .HasColumnType("TEXT"); + + b.Property("ExtraIds") + .HasColumnType("TEXT"); + + b.Property("ExtraType") + .HasColumnType("INTEGER"); + + b.Property("ForcedSortName") + .HasColumnType("TEXT"); + + b.Property("Genres") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IndexNumber") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingValue") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("IsInMixedFolder") + .HasColumnType("INTEGER"); + + b.Property("IsLocked") + .HasColumnType("INTEGER"); + + b.Property("IsMovie") + .HasColumnType("INTEGER"); + + b.Property("IsRepeat") + .HasColumnType("INTEGER"); + + b.Property("IsSeries") + .HasColumnType("INTEGER"); + + b.Property("IsVirtualItem") + .HasColumnType("INTEGER"); + + b.Property("LUFS") + .HasColumnType("REAL"); + + b.Property("MediaType") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizationGain") + .HasColumnType("REAL"); + + b.Property("OfficialRating") + .HasColumnType("TEXT"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ParentIndexNumber") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataCountryCode") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataLanguage") + .HasColumnType("TEXT"); + + b.Property("PremiereDate") + .HasColumnType("TEXT"); + + b.Property("PresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("PrimaryVersionId") + .HasColumnType("TEXT"); + + b.Property("ProductionLocations") + .HasColumnType("TEXT"); + + b.Property("ProductionYear") + .HasColumnType("INTEGER"); + + b.Property("RunTimeTicks") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("TEXT"); + + b.Property("SeasonName") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("SeriesName") + .HasColumnType("TEXT"); + + b.Property("SeriesPresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("ShowId") + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Studios") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("TopParentId") + .HasColumnType("TEXT"); + + b.Property("TotalBitrate") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UnratedType") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("Path"); + + b.HasIndex("PresentationUniqueKey"); + + b.HasIndex("TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "PresentationUniqueKey"); + + b.HasIndex("Type", "TopParentId", "StartDate"); + + b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem"); + + b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName"); + + b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.ToTable("BaseItems"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Blurhash") + .HasColumnType("BLOB"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("ImageType") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemMetadataFields"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ProviderId"); + + b.HasIndex("ProviderId", "ProviderValue", "ItemId"); + + b.ToTable("BaseItemProviders"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemTrailerTypes"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ChapterIndex") + .HasColumnType("INTEGER"); + + b.Property("ImageDateModified") + .HasColumnType("TEXT"); + + b.Property("ImagePath") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("StartPositionTicks") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "ChapterIndex"); + + b.ToTable("Chapters"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => + { + b.Property("ItemValueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CleanValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId"); + + b.HasIndex("Type", "CleanValue"); + + b.ToTable("ItemValues"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => + { + b.Property("ItemValueId") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("ItemValuesMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EndTicks") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("SegmentProviderId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartTicks") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSegments"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("AspectRatio") + .HasColumnType("TEXT"); + + b.Property("AverageFrameRate") + .HasColumnType("REAL"); + + b.Property("BitDepth") + .HasColumnType("INTEGER"); + + b.Property("BitRate") + .HasColumnType("INTEGER"); + + b.Property("BlPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("ChannelLayout") + .HasColumnType("TEXT"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CodecTimeBase") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ColorPrimaries") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ColorSpace") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ColorTransfer") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DvBlSignalCompatibilityId") + .HasColumnType("INTEGER"); + + b.Property("DvLevel") + .HasColumnType("INTEGER"); + + b.Property("DvProfile") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMajor") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMinor") + .HasColumnType("INTEGER"); + + b.Property("ElPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IsAnamorphic") + .HasColumnType("INTEGER"); + + b.Property("IsAvc") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsExternal") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("IsHearingImpaired") + .HasColumnType("INTEGER"); + + b.Property("IsInterlaced") + .HasColumnType("INTEGER"); + + b.Property("KeyFrames") + .HasColumnType("TEXT"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("REAL"); + + b.Property("NalLengthSize") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PixelFormat") + .HasColumnType("TEXT"); + + b.Property("Profile") + .HasColumnType("TEXT"); + + b.Property("RealFrameRate") + .HasColumnType("REAL"); + + b.Property("RefFrames") + .HasColumnType("INTEGER"); + + b.Property("Rotation") + .HasColumnType("INTEGER"); + + b.Property("RpuPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("SampleRate") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .HasColumnType("INTEGER"); + + b.Property("TimeBase") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "StreamIndex"); + + b.HasIndex("StreamIndex"); + + b.HasIndex("StreamType"); + + b.HasIndex("StreamIndex", "StreamType"); + + b.HasIndex("StreamIndex", "StreamType", "Language"); + + b.ToTable("MediaStreamInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.People", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PersonType") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Peoples"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("PeopleId") + .HasColumnType("TEXT"); + + b.Property("ListOrder") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.HasIndex("ItemId", "ListOrder"); + + b.HasIndex("ItemId", "SortOrder"); + + b.ToTable("PeopleBaseItemMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("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("ItemId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("CustomDataKey") + .HasColumnType("TEXT"); + + b.Property("AudioStreamIndex") + .HasColumnType("INTEGER"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER"); + + b.Property("LastPlayedDate") + .HasColumnType("TEXT"); + + b.Property("Likes") + .HasColumnType("INTEGER"); + + b.Property("PlayCount") + .HasColumnType("INTEGER"); + + b.Property("PlaybackPositionTicks") + .HasColumnType("INTEGER"); + + b.Property("Played") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("SubtitleStreamIndex") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "UserId", "CustomDataKey"); + + b.HasIndex("UserId"); + + b.HasIndex("ItemId", "UserId", "IsFavorite"); + + b.HasIndex("ItemId", "UserId", "LastPlayedDate"); + + b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks"); + + b.HasIndex("ItemId", "UserId", "Played"); + + b.ToTable("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", null) + .WithMany("AncestorIds") + .HasForeignKey("BaseItemEntityId"); + + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem") + .WithMany() + .HasForeignKey("ParentItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ParentItem"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Images") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("LockedFields") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Provider") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("TrailerTypes") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Chapters") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("ItemValues") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue") + .WithMany("BaseItemsMap") + .HasForeignKey("ItemValueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ItemValue"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("MediaStreams") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Peoples") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.People", "People") + .WithMany("BaseItems") + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("People"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("UserData") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => + { + b.Navigation("AncestorIds"); + + b.Navigation("Chapters"); + + b.Navigation("Images"); + + b.Navigation("ItemValues"); + + b.Navigation("LockedFields"); + + b.Navigation("MediaStreams"); + + b.Navigation("Peoples"); + + b.Navigation("Provider"); + + b.Navigation("TrailerTypes"); + + b.Navigation("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => + { + b.Navigation("BaseItemsMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.People", b => + { + b.Navigation("BaseItems"); + }); + + 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/20241111135439_AddedCustomDataKeyKey.cs b/Jellyfin.Server.Implementations/Migrations/20241111135439_AddedCustomDataKeyKey.cs new file mode 100644 index 0000000000..4558d7c49c --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20241111135439_AddedCustomDataKeyKey.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// + public partial class AddedCustomDataKeyKey : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropPrimaryKey( + name: "PK_UserData", + table: "UserData"); + + migrationBuilder.AlterColumn( + name: "CustomDataKey", + table: "UserData", + type: "TEXT", + nullable: false, + defaultValue: string.Empty, + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AddPrimaryKey( + name: "PK_UserData", + table: "UserData", + columns: new[] { "ItemId", "UserId", "CustomDataKey" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropPrimaryKey( + name: "PK_UserData", + table: "UserData"); + + migrationBuilder.AlterColumn( + name: "CustomDataKey", + table: "UserData", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.AddPrimaryKey( + name: "PK_UserData", + table: "UserData", + columns: new[] { "ItemId", "UserId" }); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index 6a9d9a55aa..f3424434d6 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -1276,6 +1276,9 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property("UserId") .HasColumnType("TEXT"); + b.Property("CustomDataKey") + .HasColumnType("TEXT"); + b.Property("AudioStreamIndex") .HasColumnType("INTEGER"); @@ -1303,7 +1306,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property("SubtitleStreamIndex") .HasColumnType("INTEGER"); - b.HasKey("ItemId", "UserId"); + b.HasKey("ItemId", "UserId", "CustomDataKey"); b.HasIndex("UserId"); diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/UserDataConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/UserDataConfiguration.cs index 5ebdf8d593..7bbb28d431 100644 --- a/Jellyfin.Server.Implementations/ModelConfiguration/UserDataConfiguration.cs +++ b/Jellyfin.Server.Implementations/ModelConfiguration/UserDataConfiguration.cs @@ -13,7 +13,7 @@ public class UserDataConfiguration : IEntityTypeConfiguration /// public void Configure(EntityTypeBuilder builder) { - builder.HasKey(d => new { d.ItemId, d.UserId }); + builder.HasKey(d => new { d.ItemId, d.UserId, d.CustomDataKey }); builder.HasIndex(d => new { d.ItemId, d.UserId, d.Played }); builder.HasIndex(d => new { d.ItemId, d.UserId, d.PlaybackPositionTicks }); builder.HasIndex(d => new { d.ItemId, d.UserId, d.IsFavorite }); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index 571ac95eba..a440bc6d6c 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -107,20 +107,20 @@ public class MigrateLibraryDb : IMigrationRoutine foreach (var entity in queryResult) { var userData = GetUserData(users, entity); - if (userData.Data is null) + if (userData is null) { _logger.LogError("Was not able to migrate user data with key {0}", entity.GetString(0)); continue; } - if (!legacyBaseItemWithUserKeys.TryGetValue(userData.LegacyUserDataKey!, out var refItem)) + if (!legacyBaseItemWithUserKeys.TryGetValue(userData.CustomDataKey!, out var refItem)) { _logger.LogError("Was not able to migrate user data with key {0} because it does not reference a valid BaseItem.", entity.GetString(0)); continue; } - userData.Data.ItemId = refItem.Id; - dbContext.UserData.Add(userData.Data); + userData.ItemId = refItem.Id; + dbContext.UserData.Add(userData); } _logger.LogInformation("Try saving {0} UserData entries.", dbContext.UserData.Local.Count); @@ -289,7 +289,7 @@ public class MigrateLibraryDb : IMigrationRoutine } } - private (UserData? Data, string? LegacyUserDataKey) GetUserData(ImmutableArray users, SqliteDataReader dto) + private UserData? GetUserData(ImmutableArray users, SqliteDataReader dto) { var indexOfUser = dto.GetInt32(1); var user = users.ElementAtOrDefault(indexOfUser - 1); @@ -297,14 +297,15 @@ public class MigrateLibraryDb : IMigrationRoutine if (user is null) { _logger.LogError("Tried to find user with index '{Idx}' but there are only '{MaxIdx}' users.", indexOfUser, users.Length); - return (null, null); + return null; } var oldKey = dto.GetString(0); - return (new UserData() + return new UserData() { ItemId = Guid.NewGuid(), + CustomDataKey = oldKey, UserId = user.Id, Rating = dto.IsDBNull(2) ? null : dto.GetDouble(2), Played = dto.GetBoolean(3), @@ -317,7 +318,7 @@ public class MigrateLibraryDb : IMigrationRoutine Likes = null, User = null!, Item = null! - }, oldKey); + }; } private AncestorId GetAncestorId(SqliteDataReader reader) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 0c698bb94f..d92407a3f4 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1825,7 +1825,10 @@ namespace MediaBrowser.Controller.Entities { ArgumentNullException.ThrowIfNull(user); - var data = UserDataManager.GetUserData(user, this); + var data = UserDataManager.GetUserData(user, this) ?? new UserItemData() + { + Key = GetUserDataKeys().First(), + }; if (datePlayed.HasValue) { -- cgit v1.2.3 From b39553611d0d6702ef657f76573cefa2ee437745 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sun, 17 Nov 2024 11:03:43 +0000 Subject: Applied coding style --- .../Data/CleanDatabaseScheduledTask.cs | 4 ++-- Emby.Server.Implementations/Data/ItemTypeLookup.cs | 6 ------ Emby.Server.Implementations/Library/LibraryManager.cs | 7 ++++--- Emby.Server.Implementations/Library/MediaSourceManager.cs | 2 +- Jellyfin.Api/Controllers/InstantMixController.cs | 2 +- Jellyfin.Api/Controllers/YearsController.cs | 4 ++-- Jellyfin.Server.Implementations/Item/BaseItemRepository.cs | 12 ++++++------ Jellyfin.Server.Implementations/Item/ChapterRepository.cs | 2 +- .../Item/MediaAttachmentRepository.cs | 2 +- .../Item/MediaStreamRepository.cs | 2 +- Jellyfin.Server.Implementations/Item/PeopleRepository.cs | 4 ++-- .../MediaSegments/MediaSegmentManager.cs | 2 +- MediaBrowser.Controller/Entities/BaseItem.cs | 2 +- MediaBrowser.Controller/Entities/Folder.cs | 10 +++++----- MediaBrowser.Controller/Entities/Movies/BoxSet.cs | 4 ++-- MediaBrowser.Controller/Library/ILibraryManager.cs | 2 +- MediaBrowser.Providers/Music/ArtistMetadataService.cs | 2 +- src/Jellyfin.LiveTv/Guide/GuideManager.cs | 2 +- 18 files changed, 33 insertions(+), 38 deletions(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index 6ea7d91970..aceff8b53f 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -28,9 +28,9 @@ namespace Emby.Server.Implementations.Data _dbProvider = dbProvider; } - public Task Run(IProgress progress, CancellationToken cancellationToken) + public async Task Run(IProgress progress, CancellationToken cancellationToken) { - return CleanDeadItems(cancellationToken, progress); + await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false); } private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress progress) diff --git a/Emby.Server.Implementations/Data/ItemTypeLookup.cs b/Emby.Server.Implementations/Data/ItemTypeLookup.cs index f5db28c7ac..82c0a8b6c5 100644 --- a/Emby.Server.Implementations/Data/ItemTypeLookup.cs +++ b/Emby.Server.Implementations/Data/ItemTypeLookup.cs @@ -1,12 +1,8 @@ -using System; using System.Collections.Frozen; using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; 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,7 +10,6 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; -using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Data; @@ -23,7 +18,6 @@ public class ItemTypeLookup : IItemTypeLookup { /// public IReadOnlyList MusicGenreTypes { get; } = [ - typeof(Audio).FullName!, typeof(MusicVideo).FullName!, typeof(MusicAlbum).FullName!, diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 7e059be232..7b37011cb2 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1810,11 +1810,11 @@ namespace Emby.Server.Implementations.Library /// public void CreateItem(BaseItem item, BaseItem? parent) { - CreateItems(new[] { item }, parent, CancellationToken.None); + CreateOrUpdateItems(new[] { item }, parent, CancellationToken.None); } /// - public void CreateItems(IReadOnlyList items, BaseItem? parent, CancellationToken cancellationToken) + public void CreateOrUpdateItems(IReadOnlyList items, BaseItem? parent, CancellationToken cancellationToken) { _itemRepository.SaveItems(items, cancellationToken); @@ -2971,10 +2971,11 @@ namespace Emby.Server.Implementations.Library { if (createEntity) { - CreateItems([personEntity], null, CancellationToken.None); + CreateOrUpdateItems([personEntity], null, CancellationToken.None); } await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false); + CreateOrUpdateItems([personEntity], null, CancellationToken.None); } } } diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 2fb571a106..d0f5e60f79 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -218,7 +218,7 @@ namespace Emby.Server.Implementations.Library list.Add(source); } - return SortMediaSources(list).ToImmutableArray(); + return SortMediaSources(list).ToArray(); } /// > diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index e89e7ce26c..87a856d38e 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -397,7 +397,7 @@ public class InstantMixController : BaseJellyfinApiController if (limit.HasValue && limit < items.Count) { - items = items.Take(limit.Value).ToImmutableArray(); + items = items.Take(limit.Value).ToArray(); } var result = new QueryResult( diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index 907724e040..e709e43e26 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -113,11 +113,11 @@ public class YearsController : BaseJellyfinApiController if (userId.IsNullOrEmpty()) { - items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToImmutableArray(); + items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToArray(); } else { - items = recursive ? folder.GetRecursiveChildren(user, query) : folder.GetChildren(user, true).Where(Filter).ToImmutableArray(); + items = recursive ? folder.GetRecursiveChildren(user, query) : folder.GetChildren(user, true).Where(Filter).ToArray(); } } else diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 0183685be4..8670b06cc7 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -117,7 +117,7 @@ public sealed class BaseItemRepository( PrepareFilterQuery(filter); using var context = dbProvider.CreateDbContext(); - return ApplyQueryFilter(context.BaseItems.AsNoTracking(), context, filter).Select(e => e.Id).ToImmutableArray(); + return ApplyQueryFilter(context.BaseItems.AsNoTracking(), context, filter).Select(e => e.Id).ToArray(); } /// @@ -216,7 +216,7 @@ public sealed class BaseItemRepository( dbQuery = ApplyGroupingFilter(dbQuery, filter); dbQuery = ApplyQueryPageing(dbQuery, filter); - result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToImmutableArray(); + result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray(); result.StartIndex = filter.StartIndex ?? 0; return result; } @@ -235,7 +235,7 @@ public sealed class BaseItemRepository( dbQuery = ApplyGroupingFilter(dbQuery, filter); dbQuery = ApplyQueryPageing(dbQuery, filter); - return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToImmutableArray(); + return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray(); } private IQueryable ApplyGroupingFilter(IQueryable dbQuery, InternalItemsQuery filter) @@ -831,7 +831,7 @@ public sealed class BaseItemRepository( } // query = query.DistinctBy(e => e.CleanValue); - return query.Select(e => e.ItemValue.CleanValue).ToImmutableArray(); + return query.Select(e => e.ItemValue.CleanValue).ToArray(); } private static bool TypeRequiresDeserialization(Type type) @@ -976,10 +976,10 @@ public sealed class BaseItemRepository( }); result.StartIndex = filter.StartIndex ?? 0; - result.Items = resultQuery.ToImmutableArray().Where(e => e is not null).Select(e => + result.Items = resultQuery.ToArray().Where(e => e is not null).Select(e => { return (DeserialiseBaseItem(e.item, filter.SkipDeserialization), e.itemCount); - }).ToImmutableArray(); + }).ToArray(); return result; } diff --git a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs index dc55484c9d..16e8c205d6 100644 --- a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs +++ b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs @@ -73,7 +73,7 @@ public class ChapterRepository : IChapterRepository }) .ToList() .Select(e => Map(e.chapter, e.baseItemPath!)) - .ToImmutableArray(); + .ToArray(); } /// diff --git a/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs b/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs index c6488f3210..1557982093 100644 --- a/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs +++ b/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs @@ -40,7 +40,7 @@ public class MediaAttachmentRepository(IDbContextFactory dbPr query = query.Where(e => e.Index == filter.Index); } - return query.AsEnumerable().Select(Map).ToImmutableArray(); + return query.AsEnumerable().Select(Map).ToArray(); } private MediaAttachment Map(AttachmentStreamInfo attachment) diff --git a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs index 0617dd81ec..d6bfc1a8f7 100644 --- a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs +++ b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs @@ -51,7 +51,7 @@ public class MediaStreamRepository : IMediaStreamRepository public IReadOnlyList GetMediaStreams(MediaStreamQuery filter) { using var context = _dbProvider.CreateDbContext(); - return TranslateQuery(context.MediaStreamInfos.AsNoTracking(), filter).AsEnumerable().Select(Map).ToImmutableArray(); + return TranslateQuery(context.MediaStreamInfos.AsNoTracking(), filter).AsEnumerable().Select(Map).ToArray(); } private string? GetPathToSave(string? path) diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index 417212ba4d..d1823514a6 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -36,7 +36,7 @@ public class PeopleRepository(IDbContextFactory dbProvider, I dbQuery = dbQuery.Take(filter.Limit); } - return dbQuery.AsEnumerable().Select(Map).ToImmutableArray(); + return dbQuery.AsEnumerable().Select(Map).ToArray(); } /// @@ -51,7 +51,7 @@ public class PeopleRepository(IDbContextFactory dbProvider, I dbQuery = dbQuery.Take(filter.Limit); } - return dbQuery.Select(e => e.Name).ToImmutableArray(); + return dbQuery.Select(e => e.Name).ToArray(); } /// diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs index 151b616f7e..d0f41c6fa8 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs @@ -154,7 +154,7 @@ public class MediaSegmentManager : IMediaSegmentManager return query .OrderBy(e => e.StartTicks) .AsNoTracking() - .ToImmutableArray() + .ToArray() .Select(Map); } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index d92407a3f4..a6bc35a9f4 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1092,7 +1092,7 @@ namespace MediaBrowser.Controller.Entities return 1; }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0) .ThenByDescending(i => i, new MediaSourceWidthComparator()) - .ToImmutableArray(); + .ToArray(); } protected virtual IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources() diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 8fff7dbc4d..a13f046142 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -452,7 +452,7 @@ namespace MediaBrowser.Controller.Entities if (newItems.Count > 0) { - LibraryManager.CreateItems(newItems, this, cancellationToken); + LibraryManager.CreateOrUpdateItems(newItems, this, cancellationToken); } } else @@ -1306,7 +1306,7 @@ namespace MediaBrowser.Controller.Entities AddChildren(user, includeLinkedChildren, result, false, query); - return result.Values.ToImmutableArray(); + return result.Values.ToArray(); } protected virtual IEnumerable GetEligibleChildrenForRecursiveChildren(User user) @@ -1379,7 +1379,7 @@ namespace MediaBrowser.Controller.Entities AddChildren(user, true, result, true, query); - return result.Values.ToImmutableArray(); + return result.Values.ToArray(); } /// @@ -1407,7 +1407,7 @@ namespace MediaBrowser.Controller.Entities AddChildrenToList(result, includeLinkedChildren, true, filter); - return result.Values.ToImmutableArray(); + return result.Values.ToArray(); } /// @@ -1563,7 +1563,7 @@ namespace MediaBrowser.Controller.Entities return LinkedChildren .Select(i => new Tuple(i, GetLinkedChild(i))) .Where(i => i.Item2 is not null) - .ToImmutableArray(); + .ToArray(); } protected override async Task RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList fileSystemChildren, CancellationToken cancellationToken) diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index cb17e3fafd..d0c9f049ab 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -131,13 +131,13 @@ namespace MediaBrowser.Controller.Entities.Movies public override IReadOnlyList GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) { var children = base.GetChildren(user, includeLinkedChildren, query); - return Sort(children, user).ToImmutableArray(); + return Sort(children, user).ToArray(); } public override IReadOnlyList GetRecursiveChildren(User user, InternalItemsQuery query) { var children = base.GetRecursiveChildren(user, query); - return Sort(children, user).ToImmutableArray(); + return Sort(children, user).ToArray(); } public BoxSetInfo GetLookupInfo() diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 47b1cb16e8..8fcd5f605f 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -258,7 +258,7 @@ namespace MediaBrowser.Controller.Library /// Items to create. /// Parent of new items. /// CancellationToken to use for operation. - void CreateItems(IReadOnlyList items, BaseItem? parent, CancellationToken cancellationToken); + void CreateOrUpdateItems(IReadOnlyList items, BaseItem? parent, CancellationToken cancellationToken); /// /// Updates the item. diff --git a/MediaBrowser.Providers/Music/ArtistMetadataService.cs b/MediaBrowser.Providers/Music/ArtistMetadataService.cs index 8af6de9259..c47f9a5006 100644 --- a/MediaBrowser.Providers/Music/ArtistMetadataService.cs +++ b/MediaBrowser.Providers/Music/ArtistMetadataService.cs @@ -37,7 +37,7 @@ namespace MediaBrowser.Providers.Music Recursive = true, IsFolder = false }) - : item.GetRecursiveChildren(i => i is IHasArtist && !i.IsFolder).ToImmutableArray(); + : item.GetRecursiveChildren(i => i is IHasArtist && !i.IsFolder); } } } diff --git a/src/Jellyfin.LiveTv/Guide/GuideManager.cs b/src/Jellyfin.LiveTv/Guide/GuideManager.cs index f657422a04..ff31b71233 100644 --- a/src/Jellyfin.LiveTv/Guide/GuideManager.cs +++ b/src/Jellyfin.LiveTv/Guide/GuideManager.cs @@ -265,7 +265,7 @@ public class GuideManager : IGuideManager if (newPrograms.Count > 0) { - _libraryManager.CreateItems(newPrograms, null, cancellationToken); + _libraryManager.CreateOrUpdateItems(newPrograms, null, cancellationToken); await PrecacheImages(newPrograms, maxCacheDate).ConfigureAwait(false); } -- cgit v1.2.3 From 6e7118eff1e6bc9c5ca70d80e5ff5e6eff7c90e5 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 19 Nov 2024 15:43:18 -0500 Subject: Backport pull request #12934 from jellyfin/release-10.10.z Fix playlists Original-merge: 8bee67f1f8dab604d745b3d077330085f7f111d4 Merged-by: crobibero Backported-by: Joshua M. Boniface --- .../ConfigurationOptions.cs | 1 - .../Playlists/PlaylistManager.cs | 40 ++++++++----- Jellyfin.Api/Controllers/PlaylistsController.cs | 10 ++-- Jellyfin.Server/Migrations/MigrationRunner.cs | 3 +- .../Migrations/Routines/FixPlaylistOwner.cs | 4 +- .../Routines/RemoveDuplicatePlaylistChildren.cs | 68 ++++++++++++++++++++++ MediaBrowser.Controller/Entities/LinkedChild.cs | 7 +-- .../Extensions/ConfigurationExtensions.cs | 13 ----- .../Playlists/IPlaylistManager.cs | 3 +- 9 files changed, 107 insertions(+), 42 deletions(-) create mode 100644 Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs (limited to 'MediaBrowser.Controller/Entities') diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index 91791a1c82..a06f6e7fe9 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -17,7 +17,6 @@ namespace Emby.Server.Implementations { DefaultRedirectKey, "web/" }, { FfmpegProbeSizeKey, "1G" }, { FfmpegAnalyzeDurationKey, "200M" }, - { PlaylistsAllowDuplicatesKey, bool.FalseString }, { BindToUnixSocketKey, bool.FalseString }, { SqliteCacheSizeKey, "20000" }, { FfmpegSkipValidationKey, bool.FalseString }, diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 47ff22c0b3..daeb7fed88 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -216,14 +216,11 @@ namespace Emby.Server.Implementations.Playlists var newItems = GetPlaylistItems(newItemIds, user, options) .Where(i => i.SupportsAddingToPlaylist); - // Filter out duplicate items, if necessary - if (!_appConfig.DoPlaylistsAllowDuplicates()) - { - var existingIds = playlist.LinkedChildren.Select(c => c.ItemId).ToHashSet(); - newItems = newItems - .Where(i => !existingIds.Contains(i.Id)) - .Distinct(); - } + // Filter out duplicate items + var existingIds = playlist.LinkedChildren.Select(c => c.ItemId).ToHashSet(); + newItems = newItems + .Where(i => !existingIds.Contains(i.Id)) + .Distinct(); // Create a list of the new linked children to add to the playlist var childrenToAdd = newItems @@ -269,7 +266,7 @@ namespace Emby.Server.Implementations.Playlists var idList = entryIds.ToList(); - var removals = children.Where(i => idList.Contains(i.Item1.Id)); + var removals = children.Where(i => idList.Contains(i.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture))); playlist.LinkedChildren = children.Except(removals) .Select(i => i.Item1) @@ -286,26 +283,39 @@ namespace Emby.Server.Implementations.Playlists RefreshPriority.High); } - public async Task MoveItemAsync(string playlistId, string entryId, int newIndex) + public async Task MoveItemAsync(string playlistId, string entryId, int newIndex, Guid callingUserId) { if (_libraryManager.GetItemById(playlistId) is not Playlist playlist) { throw new ArgumentException("No Playlist exists with the supplied Id"); } + var user = _userManager.GetUserById(callingUserId); var children = playlist.GetManageableItems().ToList(); + var accessibleChildren = children.Where(c => c.Item2.IsVisible(user)).ToArray(); - var oldIndex = children.FindIndex(i => string.Equals(entryId, i.Item1.Id, StringComparison.OrdinalIgnoreCase)); + var oldIndexAll = children.FindIndex(i => string.Equals(entryId, i.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase)); + var oldIndexAccessible = accessibleChildren.FindIndex(i => string.Equals(entryId, i.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase)); - if (oldIndex == newIndex) + if (oldIndexAccessible == newIndex) { return; } - var item = playlist.LinkedChildren[oldIndex]; + var newPriorItemIndex = newIndex > oldIndexAccessible ? newIndex : newIndex - 1 < 0 ? 0 : newIndex - 1; + var newPriorItemId = accessibleChildren[newPriorItemIndex].Item1.ItemId; + var newPriorItemIndexOnAllChildren = children.FindIndex(c => c.Item1.ItemId.Equals(newPriorItemId)); + var adjustedNewIndex = newPriorItemIndexOnAllChildren + 1; - var newList = playlist.LinkedChildren.ToList(); + var item = playlist.LinkedChildren.FirstOrDefault(i => string.Equals(entryId, i.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase)); + if (item is null) + { + _logger.LogWarning("Modified item not found in playlist. ItemId: {ItemId}, PlaylistId: {PlaylistId}", item.ItemId, playlistId); + return; + } + + var newList = playlist.LinkedChildren.ToList(); newList.Remove(item); if (newIndex >= newList.Count) @@ -314,7 +324,7 @@ namespace Emby.Server.Implementations.Playlists } else { - newList.Insert(newIndex, item); + newList.Insert(adjustedNewIndex, item); } playlist.LinkedChildren = [.. newList]; diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index e6f23b1364..1ab36ccc64 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Attributes; @@ -426,7 +427,7 @@ public class PlaylistsController : BaseJellyfinApiController return Forbid(); } - await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex).ConfigureAwait(false); + await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex, callingUserId).ConfigureAwait(false); return NoContent(); } @@ -514,7 +515,8 @@ public class PlaylistsController : BaseJellyfinApiController return Forbid(); } - var items = playlist.GetManageableItems().ToArray(); + var user = _userManager.GetUserById(callingUserId); + var items = playlist.GetManageableItems().Where(i => i.Item2.IsVisible(user)).ToArray(); var count = items.Length; if (startIndex.HasValue) { @@ -529,11 +531,11 @@ public class PlaylistsController : BaseJellyfinApiController var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - var user = _userManager.GetUserById(callingUserId); + var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user); for (int index = 0; index < dtos.Count; index++) { - dtos[index].PlaylistItemId = items[index].Item1.Id; + dtos[index].PlaylistItemId = items[index].Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture); } var result = new QueryResult( diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index 9d4441ac39..2ab130eefb 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -47,7 +47,8 @@ namespace Jellyfin.Server.Migrations typeof(Routines.AddDefaultCastReceivers), typeof(Routines.UpdateDefaultPluginRepository), typeof(Routines.FixAudioData), - typeof(Routines.MoveTrickplayFiles) + typeof(Routines.MoveTrickplayFiles), + typeof(Routines.RemoveDuplicatePlaylistChildren) }; /// diff --git a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs index 3655a610d3..192c170b26 100644 --- a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs +++ b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs @@ -15,12 +15,12 @@ namespace Jellyfin.Server.Migrations.Routines; /// internal class FixPlaylistOwner : IMigrationRoutine { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly IPlaylistManager _playlistManager; public FixPlaylistOwner( - ILogger logger, + ILogger logger, ILibraryManager libraryManager, IPlaylistManager playlistManager) { diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs new file mode 100644 index 0000000000..99047b2a2a --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs @@ -0,0 +1,68 @@ +using System; +using System.Linq; +using System.Threading; + +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Playlists; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations.Routines; + +/// +/// Remove duplicate playlist entries. +/// +internal class RemoveDuplicatePlaylistChildren : IMigrationRoutine +{ + private readonly ILogger _logger; + private readonly ILibraryManager _libraryManager; + private readonly IPlaylistManager _playlistManager; + + public RemoveDuplicatePlaylistChildren( + ILogger logger, + ILibraryManager libraryManager, + IPlaylistManager playlistManager) + { + _logger = logger; + _libraryManager = libraryManager; + _playlistManager = playlistManager; + } + + /// + public Guid Id => Guid.Parse("{96C156A2-7A13-4B3B-A8B8-FB80C94D20C0}"); + + /// + public string Name => "RemoveDuplicatePlaylistChildren"; + + /// + public bool PerformOnNewInstall => false; + + /// + public void Perform() + { + var playlists = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = [BaseItemKind.Playlist] + }) + .Cast() + .ToArray(); + + if (playlists.Length > 0) + { + foreach (var playlist in playlists) + { + var linkedChildren = playlist.LinkedChildren; + if (linkedChildren.Length > 0) + { + var nullItemChildren = linkedChildren.Where(c => c.ItemId is null); + var deduplicatedChildren = linkedChildren.DistinctBy(c => c.ItemId); + var newLinkedChildren = nullItemChildren.Concat(deduplicatedChildren); + playlist.LinkedChildren = linkedChildren; + playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult(); + _playlistManager.SavePlaylistFile(playlist); + } + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/LinkedChild.cs b/MediaBrowser.Controller/Entities/LinkedChild.cs index fd5fef3dc5..98e4f525f5 100644 --- a/MediaBrowser.Controller/Entities/LinkedChild.cs +++ b/MediaBrowser.Controller/Entities/LinkedChild.cs @@ -4,7 +4,6 @@ using System; using System.Globalization; -using System.Text.Json.Serialization; namespace MediaBrowser.Controller.Entities { @@ -12,7 +11,6 @@ namespace MediaBrowser.Controller.Entities { public LinkedChild() { - Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); } public string Path { get; set; } @@ -21,9 +19,6 @@ namespace MediaBrowser.Controller.Entities public string LibraryItemId { get; set; } - [JsonIgnore] - public string Id { get; set; } - /// /// Gets or sets the linked item id. /// @@ -31,6 +26,8 @@ namespace MediaBrowser.Controller.Entities public static LinkedChild Create(BaseItem item) { + ArgumentNullException.ThrowIfNull(item); + var child = new LinkedChild { Path = item.Path, diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs index f8049cd488..e4806109a1 100644 --- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs +++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs @@ -49,11 +49,6 @@ namespace MediaBrowser.Controller.Extensions /// public const string FfmpegPathKey = "ffmpeg"; - /// - /// The key for a setting that indicates whether playlists should allow duplicate entries. - /// - public const string PlaylistsAllowDuplicatesKey = "playlists:allowDuplicates"; - /// /// The key for a setting that indicates whether kestrel should bind to a unix socket. /// @@ -120,14 +115,6 @@ namespace MediaBrowser.Controller.Extensions public static bool GetFFmpegImgExtractPerfTradeoff(this IConfiguration configuration) => configuration.GetValue(FfmpegImgExtractPerfTradeoffKey); - /// - /// Gets a value indicating whether playlists should allow duplicate entries from the . - /// - /// The configuration to read the setting from. - /// True if playlists should allow duplicates, otherwise false. - public static bool DoPlaylistsAllowDuplicates(this IConfiguration configuration) - => configuration.GetValue(PlaylistsAllowDuplicatesKey); - /// /// Gets a value indicating whether kestrel should bind to a unix socket from the . /// diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index 038cbd2d67..497c4a511e 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -92,8 +92,9 @@ namespace MediaBrowser.Controller.Playlists /// The playlist identifier. /// The entry identifier. /// The new index. + /// The calling user. /// Task. - Task MoveItemAsync(string playlistId, string entryId, int newIndex); + Task MoveItemAsync(string playlistId, string entryId, int newIndex, Guid callingUserId); /// /// Removed all playlists of a user. -- cgit v1.2.3 From 2614fecf8df6e04b0d0a2b33722923c239ed0f91 Mon Sep 17 00:00:00 2001 From: Daniyar Alpyspayev Date: Thu, 12 Dec 2024 18:10:06 +0500 Subject: move to new System.Threading.Lock type for better performance --- Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs | 3 ++- Emby.Server.Implementations/Devices/DeviceId.cs | 3 ++- Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs | 2 +- Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs | 2 +- Emby.Server.Implementations/IO/FileRefresher.cs | 4 ++-- Emby.Server.Implementations/Library/LibraryManager.cs | 4 ++-- Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs | 2 +- Emby.Server.Implementations/Session/SessionWebSocketListener.cs | 2 +- Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs | 2 +- Emby.Server.Implementations/Updates/InstallationManager.cs | 2 +- MediaBrowser.Common/Plugins/BasePluginOfT.cs | 5 +++-- MediaBrowser.Controller/Entities/AggregateFolder.cs | 2 +- MediaBrowser.Controller/Entities/UserRootFolder.cs | 2 +- MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs | 4 ++-- MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs | 2 +- MediaBrowser.Controller/Session/SessionInfo.cs | 2 +- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 2 +- MediaBrowser.Providers/Manager/ProviderManager.cs | 2 +- src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs | 3 ++- src/Jellyfin.Networking/Manager/NetworkManager.cs | 6 +++--- 20 files changed, 30 insertions(+), 26 deletions(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index 9e98d5ce09..9bc3a0204b 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Threading; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; @@ -19,7 +20,7 @@ namespace Emby.Server.Implementations.AppBase public abstract class BaseConfigurationManager : IConfigurationManager { private readonly ConcurrentDictionary _configurations = new(); - private readonly object _configurationSyncLock = new(); + private readonly Lock _configurationSyncLock = new(); private ConfigurationStore[] _configurationStores = Array.Empty(); private IConfigurationFactory[] _configurationFactories = Array.Empty(); diff --git a/Emby.Server.Implementations/Devices/DeviceId.cs b/Emby.Server.Implementations/Devices/DeviceId.cs index 2459178d81..0b3c3bbd4f 100644 --- a/Emby.Server.Implementations/Devices/DeviceId.cs +++ b/Emby.Server.Implementations/Devices/DeviceId.cs @@ -4,6 +4,7 @@ using System; using System.Globalization; using System.IO; using System.Text; +using System.Threading; using MediaBrowser.Common.Configuration; using Microsoft.Extensions.Logging; @@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Devices { private readonly IApplicationPaths _appPaths; private readonly ILogger _logger; - private readonly object _syncLock = new object(); + private readonly Lock _syncLock = new(); private string? _id; diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 4c668379c8..fb0a55135f 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -34,7 +34,7 @@ public sealed class LibraryChangedNotifier : IHostedService, IDisposable private readonly IUserManager _userManager; private readonly ILogger _logger; - private readonly object _libraryChangedSyncLock = new(); + private readonly Lock _libraryChangedSyncLock = new(); private readonly List _foldersAddedTo = new(); private readonly List _foldersRemovedFrom = new(); private readonly List _itemsAdded = new(); diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs index aef02ce6bf..aa1c3064bc 100644 --- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs @@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.EntryPoints private readonly IUserManager _userManager; private readonly Dictionary> _changedItems = new(); - private readonly object _syncLock = new(); + private readonly Lock _syncLock = new(); private Timer? _updateTimer; diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index e75cab64c9..7378cf8851 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -18,8 +18,8 @@ namespace Emby.Server.Implementations.IO private readonly ILibraryManager _libraryManager; private readonly IServerConfigurationManager _configurationManager; - private readonly List _affectedPaths = new List(); - private readonly object _timerLock = new object(); + private readonly List _affectedPaths = new(); + private readonly Lock _timerLock = new(); private Timer? _timer; private bool _disposed; diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 28f7ed6598..2d1af82b31 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -81,8 +81,8 @@ namespace Emby.Server.Implementations.Library /// /// The _root folder sync lock. /// - private readonly object _rootFolderSyncLock = new object(); - private readonly object _userRootFolderSyncLock = new object(); + private readonly Lock _rootFolderSyncLock = new(); + private readonly Lock _userRootFolderSyncLock = new(); private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24); diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index fe769baf92..0bc67bc47d 100644 --- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.ScheduledTasks private readonly IApplicationPaths _applicationPaths; private readonly ILogger _logger; private readonly ITaskManager _taskManager; - private readonly object _lastExecutionResultSyncLock = new(); + private readonly Lock _lastExecutionResultSyncLock = new(); private bool _readFromFile; private TaskResult _lastExecutionResult; private Task _currentTask; diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index aba51de8f5..c4f6a6285b 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -41,7 +41,7 @@ namespace Emby.Server.Implementations.Session /// /// Lock used for accessing the WebSockets watchlist. /// - private readonly object _webSocketsLock = new object(); + private readonly Lock _webSocketsLock = new(); private readonly ISessionManager _sessionManager; private readonly ILogger _logger; diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs index 00c655634a..fdfff8f3b8 100644 --- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs @@ -67,7 +67,7 @@ namespace Emby.Server.Implementations.SyncPlay /// /// This lock has priority on locks made on . /// - private readonly object _groupsLock = new object(); + private readonly Lock _groupsLock = new(); private bool _disposed = false; diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index ce3d6cab88..c4d697be5b 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.Updates /// /// The application host. private readonly IServerApplicationHost _applicationHost; - private readonly object _currentInstallationsLock = new object(); + private readonly Lock _currentInstallationsLock = new(); /// /// The current installations. diff --git a/MediaBrowser.Common/Plugins/BasePluginOfT.cs b/MediaBrowser.Common/Plugins/BasePluginOfT.cs index bf2f12cb9b..58992ecd73 100644 --- a/MediaBrowser.Common/Plugins/BasePluginOfT.cs +++ b/MediaBrowser.Common/Plugins/BasePluginOfT.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Runtime.InteropServices; +using System.Threading; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; @@ -20,12 +21,12 @@ namespace MediaBrowser.Common.Plugins /// /// The configuration sync lock. /// - private readonly object _configurationSyncLock = new object(); + private readonly Lock _configurationSyncLock = new(); /// /// The configuration save lock. /// - private readonly object _configurationSaveLock = new object(); + private readonly Lock _configurationSaveLock = new(); /// /// The configuration. diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs index 40cdd6c91e..5e0d1bb455 100644 --- a/MediaBrowser.Controller/Entities/AggregateFolder.cs +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -23,7 +23,7 @@ namespace MediaBrowser.Controller.Entities /// public class AggregateFolder : Folder { - private readonly object _childIdsLock = new object(); + private readonly Lock _childIdsLock = new(); /// /// The _virtual children. diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs index a687adeddc..65d81b23e4 100644 --- a/MediaBrowser.Controller/Entities/UserRootFolder.cs +++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs @@ -21,7 +21,7 @@ namespace MediaBrowser.Controller.Entities /// public class UserRootFolder : Folder { - private readonly object _childIdsLock = new object(); + private readonly Lock _childIdsLock = new(); private List _childrenIds = null; /// diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs index fefa66cdb8..56990d0b82 100644 --- a/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs +++ b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs @@ -12,8 +12,8 @@ namespace MediaBrowser.Controller.MediaEncoding; public sealed class TranscodingJob : IDisposable { private readonly ILogger _logger; - private readonly object _processLock = new(); - private readonly object _timerLock = new(); + private readonly Lock _processLock = new(); + private readonly Lock _timerLock = new(); private Timer? _killTimer; diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index a47d2fa45d..4757bfa303 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -33,7 +33,7 @@ namespace MediaBrowser.Controller.Net SingleWriter = false }); - private readonly object _activeConnectionsLock = new(); + private readonly Lock _activeConnectionsLock = new(); /// /// The _active connections. diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index 3ba1bfce42..cbef5d0113 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -25,7 +25,7 @@ namespace MediaBrowser.Controller.Session private readonly ISessionManager _sessionManager; private readonly ILogger _logger; - private readonly object _progressLock = new(); + private readonly Lock _progressLock = new(); private Timer _progressTimer; private PlaybackProgressInfo _lastProgressInfo; diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index a34238cd68..e084bda27a 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -62,7 +62,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private readonly AsyncNonKeyedLocker _thumbnailResourcePool; - private readonly object _runningProcessesLock = new object(); + private readonly Lock _runningProcessesLock = new(); private readonly List _runningProcesses = new List(); // MediaEncoder is registered as a Singleton diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 010e9c3b63..854ac6b9c9 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -48,7 +48,7 @@ namespace MediaBrowser.Providers.Manager /// public class ProviderManager : IProviderManager, IDisposable { - private readonly object _refreshQueueLock = new(); + private readonly Lock _refreshQueueLock = new(); private readonly ILogger _logger; private readonly IHttpClientFactory _httpClientFactory; private readonly ILibraryMonitor _libraryMonitor; diff --git a/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs b/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs index 9e7323f5b9..6a68b8c25c 100644 --- a/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs +++ b/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text.Json; +using System.Threading; using Jellyfin.Extensions.Json; using Microsoft.Extensions.Logging; @@ -15,7 +16,7 @@ namespace Jellyfin.LiveTv.Timers where T : class { private readonly string _dataPath; - private readonly object _fileDataLock = new object(); + private readonly Lock _fileDataLock = new(); private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; private T[]? _items; diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index 10aed673b1..b1fc5d406c 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -27,7 +27,7 @@ public class NetworkManager : INetworkManager, IDisposable /// /// Threading lock for network properties. /// - private readonly object _initLock; + private readonly Lock _initLock; private readonly ILogger _logger; @@ -35,7 +35,7 @@ public class NetworkManager : INetworkManager, IDisposable private readonly IConfiguration _startupConfig; - private readonly object _networkEventLock; + private readonly Lock _networkEventLock; /// /// Holds the published server URLs and the IPs to use them on. @@ -93,7 +93,7 @@ public class NetworkManager : INetworkManager, IDisposable _interfaces = new List(); _macAddresses = new List(); _publishedServerUrls = new List(); - _networkEventLock = new object(); + _networkEventLock = new(); _remoteAddressFilter = new List(); _ = bool.TryParse(startupConfig[DetectNetworkChangeKey], out var detectNetworkChange); -- cgit v1.2.3 From 47f798827b956dbacfed2a870bab02e7ffc0da12 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 22 Jan 2025 17:31:52 +0100 Subject: Remove useless checks and dead code --- .../Library/LibraryManager.cs | 9 +------ .../Library/UserViewManager.cs | 30 ++++++++++------------ Jellyfin.Api/Auth/CustomAuthenticationHandler.cs | 2 +- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 2 +- MediaBrowser.Common/Net/NetworkUtils.cs | 3 ++- MediaBrowser.Controller/Entities/Folder.cs | 5 ---- MediaBrowser.Controller/Entities/TV/Season.cs | 2 +- MediaBrowser.Model/Dlna/StreamBuilder.cs | 8 +++--- .../MediaInfo/FFProbeVideoInfo.cs | 2 +- src/Jellyfin.LiveTv/IO/EncodedRecorder.cs | 30 +--------------------- 10 files changed, 25 insertions(+), 68 deletions(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 2d1af82b31..4a6f1716d8 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -751,14 +751,7 @@ namespace Emby.Server.Implementations.Library if (folder.Id.IsEmpty()) { - if (string.IsNullOrEmpty(folder.Path)) - { - folder.Id = GetNewItemId(folder.GetType().Name, folder.GetType()); - } - else - { - folder.Id = GetNewItemId(folder.Path, folder.GetType()); - } + folder.Id = GetNewItemId(folder.Path, folder.GetType()); } var dbItem = GetItemById(folder.Id) as BasePluginFolder; diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index e9cf47d462..d42a0e7d28 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -308,39 +308,40 @@ namespace Emby.Server.Implementations.Library } } - var mediaTypes = new List(); + MediaType[] mediaTypes = []; if (includeItemTypes.Length == 0) { + HashSet tmpMediaTypes = []; foreach (var parent in parents.OfType()) { switch (parent.CollectionType) { case CollectionType.books: - mediaTypes.Add(MediaType.Book); - mediaTypes.Add(MediaType.Audio); + tmpMediaTypes.Add(MediaType.Book); + tmpMediaTypes.Add(MediaType.Audio); break; case CollectionType.music: - mediaTypes.Add(MediaType.Audio); + tmpMediaTypes.Add(MediaType.Audio); break; case CollectionType.photos: - mediaTypes.Add(MediaType.Photo); - mediaTypes.Add(MediaType.Video); + tmpMediaTypes.Add(MediaType.Photo); + tmpMediaTypes.Add(MediaType.Video); break; case CollectionType.homevideos: - mediaTypes.Add(MediaType.Photo); - mediaTypes.Add(MediaType.Video); + tmpMediaTypes.Add(MediaType.Photo); + tmpMediaTypes.Add(MediaType.Video); break; default: - mediaTypes.Add(MediaType.Video); + tmpMediaTypes.Add(MediaType.Video); break; } } - mediaTypes = mediaTypes.Distinct().ToList(); + mediaTypes = tmpMediaTypes.ToArray(); } - var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0 + var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Length == 0 ? new[] { BaseItemKind.Person, @@ -366,14 +367,9 @@ namespace Emby.Server.Implementations.Library Limit = limit * 5, IsPlayed = isPlayed, DtoOptions = options, - MediaTypes = mediaTypes.ToArray() + MediaTypes = mediaTypes }; - if (parents.Count == 0) - { - return _libraryManager.GetItemList(query, false); - } - return _libraryManager.GetItemList(query, parents); } } diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs index 2853e69b01..c2398f71b2 100644 --- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs +++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs @@ -57,7 +57,7 @@ namespace Jellyfin.Api.Auth var claims = new[] { - new Claim(ClaimTypes.Name, authorizationInfo.User?.Username ?? string.Empty), + new Claim(ClaimTypes.Name, authorizationInfo.User.Username), new Claim(ClaimTypes.Role, role), new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)), new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId), diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 9802be7f40..6221836fc7 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -616,7 +616,7 @@ public class DynamicHlsHelper && state.VideoStream is not null && state.VideoStream.Level.HasValue) { - levelString = state.VideoStream.Level.Value.ToString(CultureInfo.InvariantCulture) ?? string.Empty; + levelString = state.VideoStream.Level.Value.ToString(CultureInfo.InvariantCulture); } else { diff --git a/MediaBrowser.Common/Net/NetworkUtils.cs b/MediaBrowser.Common/Net/NetworkUtils.cs index e482089f0a..7380963520 100644 --- a/MediaBrowser.Common/Net/NetworkUtils.cs +++ b/MediaBrowser.Common/Net/NetworkUtils.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Net; using System.Net.Sockets; using System.Text.RegularExpressions; @@ -102,7 +103,7 @@ public static partial class NetworkUtils Span bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? NetworkConstants.IPv4MaskBytes : NetworkConstants.IPv6MaskBytes]; if (!mask.TryWriteBytes(bytes, out var bytesWritten)) { - Console.WriteLine("Unable to write address bytes, only ${bytesWritten} bytes written."); + Console.WriteLine("Unable to write address bytes, only {0} bytes written.", bytesWritten.ToString(CultureInfo.InvariantCulture)); } var zeroed = false; diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 83c19a54e1..f3d893a2e4 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1240,11 +1240,6 @@ namespace MediaBrowser.Controller.Entities return false; } - if (request.GenreIds.Count > 0) - { - return false; - } - if (request.VideoTypes.Length > 0) { return false; diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 181b9be2bf..c717c5cbb7 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -132,7 +132,7 @@ namespace MediaBrowser.Controller.Entities.TV var series = Series; if (series is not null) { - return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000", CultureInfo.InvariantCulture); + return series.PresentationUniqueKey + "-" + IndexNumber.Value.ToString("000", CultureInfo.InvariantCulture); } } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 767e012029..8771e4f1c8 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1087,12 +1087,12 @@ namespace MediaBrowser.Model.Dlna _logger.LogDebug( "Transcode Result for Profile: {Profile}, Path: {Path}, PlayMethod: {PlayMethod}, AudioStreamIndex: {AudioStreamIndex}, SubtitleStreamIndex: {SubtitleStreamIndex}, Reasons: {TranscodeReason}", - options.Profile?.Name ?? "Anonymous Profile", + options.Profile.Name ?? "Anonymous Profile", item.Path ?? "Unknown path", - playlistItem?.PlayMethod, + playlistItem.PlayMethod, audioStream?.Index, - playlistItem?.SubtitleStreamIndex, - playlistItem?.TranscodeReasons); + playlistItem.SubtitleStreamIndex, + playlistItem.TranscodeReasons); } private static int GetDefaultAudioBitrate(string? audioCodec, int? audioChannels) diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 246ba2733f..8d94725cd7 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -354,7 +354,7 @@ namespace MediaBrowser.Providers.MediaInfo blurayVideoStream.Codec = ffmpegVideoStream.Codec; blurayVideoStream.BitRate = blurayVideoStream.BitRate.GetValueOrDefault() == 0 ? ffmpegVideoStream.BitRate : blurayVideoStream.BitRate; blurayVideoStream.Width = blurayVideoStream.Width.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Width; - blurayVideoStream.Height = blurayVideoStream.Height.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Height; + blurayVideoStream.Height = blurayVideoStream.Height.GetValueOrDefault() == 0 ? ffmpegVideoStream.Height : blurayVideoStream.Height; blurayVideoStream.ColorRange = ffmpegVideoStream.ColorRange; blurayVideoStream.ColorSpace = ffmpegVideoStream.ColorSpace; blurayVideoStream.ColorTransfer = ffmpegVideoStream.ColorTransfer; diff --git a/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs b/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs index 0c660637fd..c04954207b 100644 --- a/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs +++ b/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs @@ -124,22 +124,7 @@ namespace Jellyfin.LiveTv.IO private string GetCommandLineArgs(MediaSourceInfo mediaSource, string inputTempFile, string targetFile) { - string videoArgs; - if (EncodeVideo(mediaSource)) - { - const int MaxBitrate = 25000000; - videoArgs = string.Format( - CultureInfo.InvariantCulture, - "-codec:v:0 libx264 -force_key_frames \"expr:gte(t,n_forced*5)\" {0} -pix_fmt yuv420p -preset superfast -crf 23 -b:v {1} -maxrate {1} -bufsize ({1}*2) -profile:v high -level 41", - GetOutputSizeParam(), - MaxBitrate); - } - else - { - videoArgs = "-codec:v:0 copy"; - } - - videoArgs += " -fflags +genpts"; + string videoArgs = "-codec:v:0 copy -fflags +genpts"; var flags = new List(); if (mediaSource.IgnoreDts) @@ -205,19 +190,6 @@ namespace Jellyfin.LiveTv.IO private static string GetAudioArgs(MediaSourceInfo mediaSource) { return "-codec:a:0 copy"; - - // var audioChannels = 2; - // var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); - // if (audioStream is not null) - // { - // audioChannels = audioStream.Channels ?? audioChannels; - // } - // return "-codec:a:0 aac -strict experimental -ab 320000"; - } - - private static bool EncodeVideo(MediaSourceInfo mediaSource) - { - return false; } protected string GetOutputSizeParam() -- cgit v1.2.3 From 044cf9fb8597c6507a249d17cea443305881c4f6 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Sat, 7 Dec 2024 21:52:54 -1000 Subject: chore: fix spelling * a * acceleration * addition * altogether * api clients * artist * associated * bandwidth * cannot * capabilities * case-insensitive * case-sensitive * configuration * delimiter * dependent * diacritics * directors * enable * explicitly * filters * finish * have * hierarchy * implicit * include * information * into * its * keepalive * localization * macos * manual * matching * metadata * nonexistent * options * overridden * parsed * parser * playback * preferring * processes * processing * provider * ratings * retrieval * running * segments * separate * should * station * subdirectories * superseded * supported * system * than * the * throws * transpose * valid * was link: forum or chat rooms Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- .devcontainer/install-ffmpeg.sh | 2 +- .github/ISSUE_TEMPLATE/issue report.yml | 2 +- .../IO/ManagedFileSystem.cs | 4 +- .../Library/MediaSourceManager.cs | 8 +-- .../Localization/LocalizationManager.cs | 4 +- .../Plugins/PluginManager.cs | 14 ++-- .../ScheduledTasks/ScheduledTaskWorker.cs | 4 +- .../ScheduledTasks/Tasks/PluginUpdateTask.cs | 2 +- .../Session/SessionManager.cs | 4 +- .../Updates/InstallationManager.cs | 2 +- Jellyfin.Api/Controllers/DynamicHlsController.cs | 2 +- Jellyfin.Api/Controllers/UserLibraryController.cs | 6 +- .../Models/MediaInfoDtos/OpenLiveStreamDto.cs | 2 +- .../Models/MediaInfoDtos/PlaybackInfoDto.cs | 2 +- Jellyfin.Data/Entities/Libraries/CollectionItem.cs | 4 +- Jellyfin.Data/Entities/Libraries/Series.cs | 2 +- Jellyfin.Data/Entities/TrickplayInfo.cs | 2 +- .../Item/BaseItemRepository.cs | 2 +- .../JellyfinDbContext.cs | 2 +- .../MediaSegments/MediaSegmentManager.cs | 2 +- .../Trickplay/TrickplayManager.cs | 2 +- .../Routines/CreateUserLoggingConfigFile.cs | 2 +- .../Configuration/IConfigurationManager.cs | 2 +- MediaBrowser.Controller/Devices/IDeviceManager.cs | 4 +- MediaBrowser.Controller/Entities/BaseItem.cs | 2 +- .../Library/IMediaSourceManager.cs | 2 +- .../MediaEncoding/EncodingHelper.cs | 60 ++++++++-------- .../MediaSegements/IMediaSegmentManager.cs | 80 ---------------------- .../MediaSegements/IMediaSegmentProvider.cs | 36 ---------- .../MediaSegments/IMediaSegmentManager.cs | 80 ++++++++++++++++++++++ .../MediaSegments/IMediaSegmentProvider.cs | 36 ++++++++++ .../Net/IWebSocketConnection.cs | 4 +- .../Sorting/IUserBaseItemComparer.cs | 2 +- MediaBrowser.Controller/Streaming/StreamState.cs | 2 +- .../Images/LocalImageProvider.cs | 2 +- .../Parsers/BoxSetXmlParser.cs | 2 +- .../BdInfo/BdInfoDirectoryInfo.cs | 4 +- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 6 +- .../Probing/FFProbeHelpers.cs | 4 +- .../Configuration/ServerConfiguration.cs | 6 +- MediaBrowser.Model/Dlna/ConditionProcessor.cs | 4 +- MediaBrowser.Model/Dlna/DirectPlayProfile.cs | 2 +- .../Entities/HardwareAccelerationType.cs | 2 +- MediaBrowser.Model/Entities/MetadataProvider.cs | 2 +- .../Entities/ProviderIdsExtensions.cs | 6 +- .../Globalization/ILocalizationManager.cs | 2 +- MediaBrowser.Model/IO/IFileSystem.cs | 4 +- MediaBrowser.Model/Plugins/PluginStatus.cs | 7 +- MediaBrowser.Model/Session/TranscodingInfo.cs | 2 +- MediaBrowser.Model/System/PublicSystemInfo.cs | 2 +- MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs | 4 +- MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs | 2 +- src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs | 2 +- .../Listings/SchedulesDirectDtos/MapDto.cs | 2 +- .../SchedulesDirectDtos/ProgramDetailsDto.cs | 2 +- src/Jellyfin.Networking/Manager/NetworkManager.cs | 2 +- .../Converters/JsonCommaDelimitedArrayTests.cs | 2 +- .../StringExtensionsTests.cs | 8 +-- .../SchedulesDirectDeserializeTests.cs | 2 +- .../TV/TvParserHelpersTest.cs | 8 +-- .../Jellyfin.Networking.Tests/NetworkParseTests.cs | 2 +- .../Omdb/JsonOmdbConverterTests.cs | 4 +- .../Test Data/Updates/manifest.json | 2 +- .../Controllers/DashboardControllerTests.cs | 6 +- .../Controllers/ItemsControllerTests.cs | 2 +- .../Controllers/LibraryControllerTests.cs | 6 +- .../Controllers/PlaystateControllerTests.cs | 8 +-- .../Controllers/UserControllerTests.cs | 16 ++--- .../Controllers/UserLibraryControllerTests.cs | 6 +- .../Controllers/VideosControllerTests.cs | 2 +- .../Parsers/EpisodeNfoProviderTests.cs | 2 +- 71 files changed, 269 insertions(+), 264 deletions(-) delete mode 100644 MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs delete mode 100644 MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs create mode 100644 MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs create mode 100644 MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs (limited to 'MediaBrowser.Controller/Entities') diff --git a/.devcontainer/install-ffmpeg.sh b/.devcontainer/install-ffmpeg.sh index 842a532554..1e58e6ef44 100644 --- a/.devcontainer/install-ffmpeg.sh +++ b/.devcontainer/install-ffmpeg.sh @@ -1,6 +1,6 @@ #!/bin/bash -## configure the following for a manuall install of a specific version from the repo +## configure the following for a manual install of a specific version from the repo # wget https://repo.jellyfin.org/releases/server/ubuntu/versions/jellyfin-ffmpeg/6.0.1-1/jellyfin-ffmpeg6_6.0.1-1-jammy_amd64.deb -O ffmpeg.deb diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml index 9181a1e7da..4f58c5bc50 100644 --- a/.github/ISSUE_TEMPLATE/issue report.yml +++ b/.github/ISSUE_TEMPLATE/issue report.yml @@ -14,7 +14,7 @@ body: label: "This issue respects the following points:" description: All conditions are **required**. Failure to comply with any of these conditions may cause your issue to be closed without comment. options: - - label: This is a **bug**, not a question or a configuration issue; Please visit our forum or chat rooms first to troubleshoot with volunteers, before creating a report. The links can be found [here](https://jellyfin.org/contact/). + - label: This is a **bug**, not a question or a configuration issue; Please visit our [forum or chat rooms](https://jellyfin.org/contact/) first to troubleshoot with volunteers, before creating a report. required: true - label: This issue is **not** already reported on [GitHub](https://github.com/jellyfin/jellyfin/issues?q=is%3Aopen+is%3Aissue) _(I've searched it)_. required: true diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 4b68f21d55..46c128dedc 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -561,7 +561,7 @@ namespace Emby.Server.Implementations.IO { var enumerationOptions = GetEnumerationOptions(recursive); - // On linux and osx the search pattern is case sensitive + // On linux and macOS the search pattern is case-sensitive // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Count == 1) { @@ -618,7 +618,7 @@ namespace Emby.Server.Implementations.IO { var enumerationOptions = GetEnumerationOptions(recursive); - // On linux and osx the search pattern is case sensitive + // On linux and macOS the search pattern is case-sensitive // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Length == 1) { diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index d0f5e60f79..5795c47ccc 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -39,7 +39,7 @@ namespace Emby.Server.Implementations.Library public class MediaSourceManager : IMediaSourceManager, IDisposable { // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message. - private const char LiveStreamIdDelimeter = '_'; + private const char LiveStreamIdDelimiter = '_'; private readonly IServerApplicationHost _appHost; private readonly IItemRepository _itemRepo; @@ -313,7 +313,7 @@ namespace Emby.Server.Implementations.Library private static void SetKeyProperties(IMediaSourceProvider provider, MediaSourceInfo mediaSource) { - var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamIdDelimeter; + var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamIdDelimiter; if (!string.IsNullOrEmpty(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { @@ -866,11 +866,11 @@ namespace Emby.Server.Implementations.Library { ArgumentException.ThrowIfNullOrEmpty(key); - var keys = key.Split(LiveStreamIdDelimeter, 2); + var keys = key.Split(LiveStreamIdDelimiter, 2); var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase)); - var splitIndex = key.IndexOf(LiveStreamIdDelimeter, StringComparison.Ordinal); + var splitIndex = key.IndexOf(LiveStreamIdDelimiter, StringComparison.Ordinal); var keyId = key.Substring(splitIndex + 1); return (provider, keyId); diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index ac453a5b09..c939a5e099 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -231,13 +231,13 @@ namespace Emby.Server.Implementations.Localization ratings.Add(new ParentalRating("21", 21)); } - // A lot of countries don't excplicitly have a seperate rating for adult content + // A lot of countries don't explicitly have a separate rating for adult content if (ratings.All(x => x.Value != 1000)) { ratings.Add(new ParentalRating("XXX", 1000)); } - // A lot of countries don't excplicitly have a seperate rating for banned content + // A lot of countries don't explicitly have a separate rating for banned content if (ratings.All(x => x.Value != 1001)) { ratings.Add(new ParentalRating("Banned", 1001)); diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 4c32d57179..8eeca3667e 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -119,7 +119,7 @@ namespace Emby.Server.Implementations.Plugins // Now load the assemblies.. foreach (var plugin in _plugins) { - UpdatePluginSuperceedStatus(plugin); + UpdatePluginSupersededStatus(plugin); if (plugin.IsEnabledAndSupported == false) { @@ -214,7 +214,7 @@ namespace Emby.Server.Implementations.Plugins continue; } - UpdatePluginSuperceedStatus(plugin); + UpdatePluginSupersededStatus(plugin); if (!plugin.IsEnabledAndSupported) { continue; @@ -624,9 +624,9 @@ namespace Emby.Server.Implementations.Plugins } } - private void UpdatePluginSuperceedStatus(LocalPlugin plugin) + private void UpdatePluginSupersededStatus(LocalPlugin plugin) { - if (plugin.Manifest.Status != PluginStatus.Superceded) + if (plugin.Manifest.Status != PluginStatus.Superseded) { return; } @@ -876,7 +876,7 @@ namespace Emby.Server.Implementations.Plugins } /// - /// Changes the status of the other versions of the plugin to "Superceded". + /// Changes the status of the other versions of the plugin to "Superseded". /// /// The that's master. private void ProcessAlternative(LocalPlugin plugin) @@ -896,11 +896,11 @@ namespace Emby.Server.Implementations.Plugins return; } - if (plugin.Manifest.Status == PluginStatus.Active && !ChangePluginState(previousVersion, PluginStatus.Superceded)) + if (plugin.Manifest.Status == PluginStatus.Active && !ChangePluginState(previousVersion, PluginStatus.Superseded)) { _logger.LogError("Unable to enable version {Version} of {Name}", previousVersion.Version, previousVersion.Name); } - else if (plugin.Manifest.Status == PluginStatus.Superceded && !ChangePluginState(previousVersion, PluginStatus.Active)) + else if (plugin.Manifest.Status == PluginStatus.Superseded && !ChangePluginState(previousVersion, PluginStatus.Active)) { _logger.LogError("Unable to supercede version {Version} of {Name}", previousVersion.Version, previousVersion.Name); } diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index 0bc67bc47d..985f0a8f85 100644 --- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -543,7 +543,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { DisposeTriggers(); - var wassRunning = State == TaskState.Running; + var wasRunning = State == TaskState.Running; var startTime = CurrentExecutionStartTime; var token = CurrentCancellationTokenSource; @@ -596,7 +596,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } } - if (wassRunning) + if (wasRunning) { OnTaskCompleted(startTime, DateTime.UtcNow, TaskCompletionStatus.Aborted, null); } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs index c597103dd4..b74f4d1b25 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs @@ -88,7 +88,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks } catch (OperationCanceledException) { - // InstallPackage has it's own inner cancellation token, so only throw this if it's ours + // InstallPackage has its own inner cancellation token, so only throw this if it's ours if (cancellationToken.IsCancellationRequested) { throw; diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index fe2c3d24f6..030da6f73e 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1303,7 +1303,7 @@ namespace Emby.Server.Implementations.Session if (item is null) { - _logger.LogError("A non-existent item Id {0} was passed into TranslateItemForPlayback", id); + _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForPlayback", id); return Array.Empty(); } @@ -1356,7 +1356,7 @@ namespace Emby.Server.Implementations.Session if (item is null) { - _logger.LogError("A non-existent item Id {0} was passed into TranslateItemForInstantMix", id); + _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForInstantMix", id); return new List(); } diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index c4d697be5b..678475b31f 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -187,7 +187,7 @@ namespace Emby.Server.Implementations.Updates await _pluginManager.PopulateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false); } - // Remove versions with a target ABI greater then the current application version. + // Remove versions with a target ABI greater than the current application version. if (Version.TryParse(version.TargetAbi, out var targetAbi) && _applicationHost.ApplicationVersion < targetAbi) { package.Versions.RemoveAt(i); diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index a641ec2091..60b99c7ae4 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1778,7 +1778,7 @@ public class DynamicHlsController : BaseJellyfinApiController } else if (state.AudioStream?.CodecTag is not null && state.AudioStream.CodecTag.Equals("ac-4", StringComparison.Ordinal)) { - // ac-4 audio tends to hava a super weird sample rate that will fail most encoders + // ac-4 audio tends to have a super weird sample rate that will fail most encoders // force resample it to 48KHz args += " -ar 48000"; } diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index 272a59559f..7cce13e424 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -634,10 +634,10 @@ public class UserLibraryController : BaseJellyfinApiController { if (item is Person) { - var hasMetdata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary); - var performFullRefresh = !hasMetdata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3; + var hasMetadata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary); + var performFullRefresh = !hasMetadata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3; - if (!hasMetdata) + if (!hasMetadata) { var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { diff --git a/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs b/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs index 978e99b35c..758c89938e 100644 --- a/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs +++ b/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs @@ -61,7 +61,7 @@ public class OpenLiveStreamDto public bool? EnableDirectPlay { get; set; } /// - /// Gets or sets a value indicating whether to enale direct stream. + /// Gets or sets a value indicating whether to enable direct stream. /// public bool? EnableDirectStream { get; set; } diff --git a/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs b/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs index 82f603ca1e..73ab76817c 100644 --- a/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs +++ b/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs @@ -4,7 +4,7 @@ using MediaBrowser.Model.Dlna; namespace Jellyfin.Api.Models.MediaInfoDtos; /// -/// Plabyback info dto. +/// Playback info dto. /// public class PlaybackInfoDto { diff --git a/Jellyfin.Data/Entities/Libraries/CollectionItem.cs b/Jellyfin.Data/Entities/Libraries/CollectionItem.cs index 0cb4716dbe..15b356a74e 100644 --- a/Jellyfin.Data/Entities/Libraries/CollectionItem.cs +++ b/Jellyfin.Data/Entities/Libraries/CollectionItem.cs @@ -43,7 +43,7 @@ namespace Jellyfin.Data.Entities.Libraries /// Gets or sets the next item in the collection. /// /// - /// TODO check if this properly updated Dependant and has the proper principal relationship. + /// TODO check if this properly updated Dependent and has the proper principal relationship. /// public virtual CollectionItem? Next { get; set; } @@ -51,7 +51,7 @@ namespace Jellyfin.Data.Entities.Libraries /// Gets or sets the previous item in the collection. /// /// - /// TODO check if this properly updated Dependant and has the proper principal relationship. + /// TODO check if this properly updated Dependent and has the proper principal relationship. /// public virtual CollectionItem? Previous { get; set; } diff --git a/Jellyfin.Data/Entities/Libraries/Series.cs b/Jellyfin.Data/Entities/Libraries/Series.cs index 0354433e08..ab484c96d6 100644 --- a/Jellyfin.Data/Entities/Libraries/Series.cs +++ b/Jellyfin.Data/Entities/Libraries/Series.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; namespace Jellyfin.Data.Entities.Libraries { /// - /// An entity representing a a series. + /// An entity representing a series. /// public class Series : LibraryItem { diff --git a/Jellyfin.Data/Entities/TrickplayInfo.cs b/Jellyfin.Data/Entities/TrickplayInfo.cs index 64e7da1b5d..ff9a68beff 100644 --- a/Jellyfin.Data/Entities/TrickplayInfo.cs +++ b/Jellyfin.Data/Entities/TrickplayInfo.cs @@ -66,7 +66,7 @@ public class TrickplayInfo public int Interval { get; set; } /// - /// Gets or sets peak bandwith usage in bits per second. + /// Gets or sets peak bandwidth usage in bits per second. /// /// /// Required. diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 8516301a83..952269b7ed 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -2065,7 +2065,7 @@ public sealed class BaseItemRepository if (filter.IncludeInheritedTags.Length > 0) { // Episodes do not store inherit tags from their parents in the database, and the tag may be still required by the client. - // In addtion to the tags for the episodes themselves, we need to manually query its parent (the season)'s tags as well. + // In addition to the tags for the episodes themselves, we need to manually query its parent (the season)'s tags as well. if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode) { baseQuery = baseQuery diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs index becfd81a4a..34d9e3960d 100644 --- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs +++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs @@ -268,7 +268,7 @@ public class JellyfinDbContext(DbContextOptions options, ILog modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc); base.OnModelCreating(modelBuilder); - // Configuration for each entity is in it's own class inside 'ModelConfiguration'. + // Configuration for each entity is in its own class inside 'ModelConfiguration'. modelBuilder.ApplyConfigurationsFromAssembly(typeof(JellyfinDbContext).Assembly); } } diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs index 2d3a25357d..59ec418ce7 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs @@ -22,7 +22,7 @@ using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Implementations.MediaSegments; /// -/// Manages media segments retrival and storage. +/// Manages media segments retrieval and storage. /// public class MediaSegmentManager : IMediaSegmentManager { diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index cd73d67c3b..dfc63b63f6 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -46,7 +46,7 @@ public class TrickplayManager : ITrickplayManager /// /// The logger. /// The media encoder. - /// The file systen. + /// The file system. /// The encoding helper. /// The library manager. /// The server configuration manager. diff --git a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs index ee4f8b0bab..5a8ef2e1cd 100644 --- a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs +++ b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs @@ -46,7 +46,7 @@ namespace Jellyfin.Server.Migrations.Routines public Guid Id => Guid.Parse("{EF103419-8451-40D8-9F34-D1A8E93A1679}"); /// - public string Name => "CreateLoggingConfigHeirarchy"; + public string Name => "CreateLoggingConfigHierarchy"; /// public bool PerformOnNewInstall => false; diff --git a/MediaBrowser.Common/Configuration/IConfigurationManager.cs b/MediaBrowser.Common/Configuration/IConfigurationManager.cs index e6696a571d..18a8d3e7b7 100644 --- a/MediaBrowser.Common/Configuration/IConfigurationManager.cs +++ b/MediaBrowser.Common/Configuration/IConfigurationManager.cs @@ -61,7 +61,7 @@ namespace MediaBrowser.Common.Configuration object GetConfiguration(string key); /// - /// Gets the array of coniguration stores. + /// Gets the array of configuration stores. /// /// Array of ConfigurationStore. ConfigurationStore[] GetConfigurationStores(); diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index cade53d994..fe7dc1cf94 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -58,7 +58,7 @@ public interface IDeviceManager QueryResult GetDevices(DeviceQuery query); /// - /// Gets device infromation based on the provided query. + /// Gets device information based on the provided query. /// /// The device query. /// A representing the retrieval of the device information. @@ -109,7 +109,7 @@ public interface IDeviceManager DeviceOptionsDto? GetDeviceOptions(string deviceId); /// - /// Gets the dto for client capabilites. + /// Gets the dto for client capabilities. /// /// The client capabilities. /// of the device. diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index a6bc35a9f4..9276989b4b 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1799,7 +1799,7 @@ namespace MediaBrowser.Controller.Entities /// Adds a genre to the item. /// /// The name. - /// Throwns if name is null. + /// Throws if name is null. public void AddGenre(string name) { ArgumentException.ThrowIfNullOrEmpty(name); diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs index 729b385cfb..eb697268c7 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs @@ -53,7 +53,7 @@ namespace MediaBrowser.Controller.Library IReadOnlyList GetMediaAttachments(MediaAttachmentQuery query); /// - /// Gets the playack media sources. + /// Gets the playback media sources. /// /// Item to use. /// User to use for operation. diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 9399679a4f..ff2d2345db 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -60,7 +60,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFixedKernel60i915Hang = new Version(6, 0, 18); private readonly Version _minKernelVersionAmdVkFmtModifier = new Version(5, 15); - private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0); + private readonly Version _minFFmpegImplicitHwaccel = new Version(6, 0); private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0); private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3); private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1); @@ -631,7 +631,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (string.IsNullOrWhiteSpace(container)) { - // this may not work, but if the client is that broken we can not do anything better + // this may not work, but if the client is that broken we cannot do anything better return "aac"; } @@ -3649,8 +3649,8 @@ namespace MediaBrowser.Controller.MediaEncoding var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doCuTranspose = !string.IsNullOrEmpty(tranposeDir) && _mediaEncoder.SupportsFilter("transpose_cuda"); + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doCuTranspose = !string.IsNullOrEmpty(transposeDir) && _mediaEncoder.SupportsFilter("transpose_cuda"); var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isNvDecoder && doCuTranspose)); var swpInW = swapWAndH ? inH : inW; var swpInH = swapWAndH ? inW : inH; @@ -3696,7 +3696,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hw transpose if (doCuTranspose) { - mainFilters.Add($"transpose_cuda=dir={tranposeDir}"); + mainFilters.Add($"transpose_cuda=dir={transposeDir}"); } var isRext = IsVideoStreamHevcRext(state); @@ -3856,8 +3856,8 @@ namespace MediaBrowser.Controller.MediaEncoding var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doOclTranspose = !string.IsNullOrEmpty(tranposeDir) + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doOclTranspose = !string.IsNullOrEmpty(transposeDir) && _mediaEncoder.SupportsFilterWithOption(FilterOptionType.TransposeOpenclReversal); var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isD3d11vaDecoder && doOclTranspose)); var swpInW = swapWAndH ? inH : inW; @@ -3901,12 +3901,12 @@ namespace MediaBrowser.Controller.MediaEncoding // map from d3d11va to opencl via d3d11-opencl interop. mainFilters.Add("hwmap=derive_device=opencl:mode=read"); - // hw deint <= TODO: finsh the 'yadif_opencl' filter + // hw deint <= TODO: finish the 'yadif_opencl' filter // hw transpose if (doOclTranspose) { - mainFilters.Add($"transpose_opencl=dir={tranposeDir}"); + mainFilters.Add($"transpose_opencl=dir={transposeDir}"); } var outFormat = doOclTonemap ? string.Empty : "nv12"; @@ -4097,8 +4097,8 @@ namespace MediaBrowser.Controller.MediaEncoding var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doVppTranspose = !string.IsNullOrEmpty(tranposeDir); + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doVppTranspose = !string.IsNullOrEmpty(transposeDir); var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || ((isD3d11vaDecoder || isQsvDecoder) && doVppTranspose)); var swpInW = swapWAndH ? inH : inW; var swpInH = swapWAndH ? inW : inH; @@ -4191,7 +4191,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(hwScaleFilter) && doVppTranspose) { - hwScaleFilter += $":transpose={tranposeDir}"; + hwScaleFilter += $":transpose={transposeDir}"; } if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder) @@ -4384,8 +4384,8 @@ namespace MediaBrowser.Controller.MediaEncoding var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doVppTranspose = !string.IsNullOrEmpty(tranposeDir); + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doVppTranspose = !string.IsNullOrEmpty(transposeDir); var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || ((isVaapiDecoder || isQsvDecoder) && doVppTranspose)); var swpInW = swapWAndH ? inH : inW; var swpInH = swapWAndH ? inW : inH; @@ -4445,7 +4445,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hw transpose(vaapi vpp) if (isVaapiDecoder && doVppTranspose) { - mainFilters.Add($"transpose_vaapi=dir={tranposeDir}"); + mainFilters.Add($"transpose_vaapi=dir={transposeDir}"); } var outFormat = doTonemap ? (((isQsvDecoder && doVppTranspose) || isRext) ? "p010" : string.Empty) : "nv12"; @@ -4455,7 +4455,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(hwScaleFilter) && isQsvDecoder && doVppTranspose) { - hwScaleFilter += $":transpose={tranposeDir}"; + hwScaleFilter += $":transpose={transposeDir}"; } if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder) @@ -4715,8 +4715,8 @@ namespace MediaBrowser.Controller.MediaEncoding var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doVaVppTranspose = !string.IsNullOrEmpty(tranposeDir); + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doVaVppTranspose = !string.IsNullOrEmpty(transposeDir); var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isVaapiDecoder && doVaVppTranspose)); var swpInW = swapWAndH ? inH : inW; var swpInH = swapWAndH ? inW : inH; @@ -4771,7 +4771,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hw transpose if (doVaVppTranspose) { - mainFilters.Add($"transpose_vaapi=dir={tranposeDir}"); + mainFilters.Add($"transpose_vaapi=dir={transposeDir}"); } var outFormat = doTonemap ? (isRext ? "p010" : string.Empty) : "nv12"; @@ -4948,8 +4948,8 @@ namespace MediaBrowser.Controller.MediaEncoding || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doVkTranspose = isVaapiDecoder && !string.IsNullOrEmpty(tranposeDir); + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doVkTranspose = isVaapiDecoder && !string.IsNullOrEmpty(transposeDir); var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isVaapiDecoder && doVkTranspose)); var swpInW = swapWAndH ? inH : inW; var swpInH = swapWAndH ? inW : inH; @@ -5042,13 +5042,13 @@ namespace MediaBrowser.Controller.MediaEncoding // vk transpose if (doVkTranspose) { - if (string.Equals(tranposeDir, "reversal", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(transposeDir, "reversal", StringComparison.OrdinalIgnoreCase)) { mainFilters.Add("flip_vulkan"); } else { - mainFilters.Add($"transpose_vulkan=dir={tranposeDir}"); + mainFilters.Add($"transpose_vulkan=dir={transposeDir}"); } } @@ -5416,8 +5416,8 @@ namespace MediaBrowser.Controller.MediaEncoding var usingHwSurface = isVtDecoder && (_mediaEncoder.EncoderVersion >= _minFFmpegWorkingVtHwSurface); var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doVtTranspose = !string.IsNullOrEmpty(tranposeDir) && _mediaEncoder.SupportsFilter("transpose_vt"); + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doVtTranspose = !string.IsNullOrEmpty(transposeDir) && _mediaEncoder.SupportsFilter("transpose_vt"); var swapWAndH = Math.Abs(rotation) == 90 && doVtTranspose; var swpInW = swapWAndH ? inH : inW; var swpInH = swapWAndH ? inW : inH; @@ -5461,7 +5461,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hw transpose if (doVtTranspose) { - mainFilters.Add($"transpose_vt=dir={tranposeDir}"); + mainFilters.Add($"transpose_vt=dir={transposeDir}"); } if (doVtTonemap) @@ -5624,8 +5624,8 @@ namespace MediaBrowser.Controller.MediaEncoding var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doRkVppTranspose = !string.IsNullOrEmpty(tranposeDir); + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doRkVppTranspose = !string.IsNullOrEmpty(transposeDir); var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isRkmppDecoder && doRkVppTranspose)); var swpInW = swapWAndH ? inH : inW; var swpInH = swapWAndH ? inW : inH; @@ -5696,7 +5696,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(hwScaleFilter) && doRkVppTranspose) { - hwScaleFilter += $":transpose={tranposeDir}"; + hwScaleFilter += $":transpose={transposeDir}"; } // try enabling AFBC to save DDR bandwidth @@ -6170,7 +6170,7 @@ namespace MediaBrowser.Controller.MediaEncoding var ffmpegVersion = _mediaEncoder.EncoderVersion; // Set the av1 codec explicitly to trigger hw accelerator, otherwise libdav1d will be used. - var isAv1 = ffmpegVersion < _minFFmpegImplictHwaccel + var isAv1 = ffmpegVersion < _minFFmpegImplicitHwaccel && string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase); // Allow profile mismatch if decoding H.264 baseline with d3d11va and vaapi hwaccels. diff --git a/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs b/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs deleted file mode 100644 index 672f27eca2..0000000000 --- a/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.MediaSegments; - -namespace MediaBrowser.Controller; - -/// -/// Defines methods for interacting with media segments. -/// -public interface IMediaSegmentManager -{ - /// - /// Uses all segment providers enabled for the 's library to get the Media Segments. - /// - /// The Item to evaluate. - /// If set, will remove existing segments and replace it with new ones otherwise will check for existing segments and if found any, stops. - /// stop request token. - /// A task that indicates the Operation is finished. - Task RunSegmentPluginProviders(BaseItem baseItem, bool overwrite, CancellationToken cancellationToken); - - /// - /// Returns if this item supports media segments. - /// - /// The base Item to check. - /// True if supported otherwise false. - bool IsTypeSupported(BaseItem baseItem); - - /// - /// Creates a new Media Segment associated with an Item. - /// - /// The segment to create. - /// The id of the Provider who created this segment. - /// The created Segment entity. - Task CreateSegmentAsync(MediaSegmentDto mediaSegment, string segmentProviderId); - - /// - /// Deletes a single media segment. - /// - /// The to delete. - /// a task. - Task DeleteSegmentAsync(Guid segmentId); - - /// - /// Obtains all segments accociated with the itemId. - /// - /// The id of the . - /// filteres all media segments of the given type to be included. If null all types are included. - /// When set filteres the segments to only return those that which providers are currently enabled on their library. - /// An enumerator of 's. - Task> GetSegmentsAsync(Guid itemId, IEnumerable? typeFilter, bool filterByProvider = true); - - /// - /// Obtains all segments accociated with the itemId. - /// - /// The . - /// filteres all media segments of the given type to be included. If null all types are included. - /// When set filteres the segments to only return those that which providers are currently enabled on their library. - /// An enumerator of 's. - Task> GetSegmentsAsync(BaseItem item, IEnumerable? typeFilter, bool filterByProvider = true); - - /// - /// Gets information about any media segments stored for the given itemId. - /// - /// The id of the . - /// True if there are any segments stored for the item, otherwise false. - /// TODO: this should be async but as the only caller BaseItem.GetVersionInfo isn't async, this is also not. Venson. - bool HasSegments(Guid itemId); - - /// - /// Gets a list of all registered Segment Providers and their IDs. - /// - /// The media item that should be tested for providers. - /// A list of all providers for the tested item. - IEnumerable<(string Name, string Id)> GetSupportedProviders(BaseItem item); -} diff --git a/MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs b/MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs deleted file mode 100644 index 39bb58bef2..0000000000 --- a/MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Model; -using MediaBrowser.Model.MediaSegments; - -namespace MediaBrowser.Controller; - -/// -/// Provides methods for Obtaining the Media Segments from an Item. -/// -public interface IMediaSegmentProvider -{ - /// - /// Gets the provider name. - /// - string Name { get; } - - /// - /// Enumerates all Media Segments from an Media Item. - /// - /// Arguments to enumerate MediaSegments. - /// Abort token. - /// A list of all MediaSegments found from this provider. - Task> GetMediaSegments(MediaSegmentGenerationRequest request, CancellationToken cancellationToken); - - /// - /// Should return support state for the given item. - /// - /// The base item to extract segments from. - /// True if item is supported, otherwise false. - ValueTask Supports(BaseItem item); -} diff --git a/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs new file mode 100644 index 0000000000..570d2bacea --- /dev/null +++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.MediaSegments; + +namespace MediaBrowser.Controller; + +/// +/// Defines methods for interacting with media segments. +/// +public interface IMediaSegmentManager +{ + /// + /// Uses all segment providers enabled for the 's library to get the Media Segments. + /// + /// The Item to evaluate. + /// If set, will remove existing segments and replace it with new ones otherwise will check for existing segments and if found any, stops. + /// stop request token. + /// A task that indicates the Operation is finished. + Task RunSegmentPluginProviders(BaseItem baseItem, bool overwrite, CancellationToken cancellationToken); + + /// + /// Returns if this item supports media segments. + /// + /// The base Item to check. + /// True if supported otherwise false. + bool IsTypeSupported(BaseItem baseItem); + + /// + /// Creates a new Media Segment associated with an Item. + /// + /// The segment to create. + /// The id of the Provider who created this segment. + /// The created Segment entity. + Task CreateSegmentAsync(MediaSegmentDto mediaSegment, string segmentProviderId); + + /// + /// Deletes a single media segment. + /// + /// The to delete. + /// a task. + Task DeleteSegmentAsync(Guid segmentId); + + /// + /// Obtains all segments associated with the itemId. + /// + /// The id of the . + /// filters all media segments of the given type to be included. If null all types are included. + /// When set filters the segments to only return those that which providers are currently enabled on their library. + /// An enumerator of 's. + Task> GetSegmentsAsync(Guid itemId, IEnumerable? typeFilter, bool filterByProvider = true); + + /// + /// Obtains all segments associated with the itemId. + /// + /// The . + /// filters all media segments of the given type to be included. If null all types are included. + /// When set filters the segments to only return those that which providers are currently enabled on their library. + /// An enumerator of 's. + Task> GetSegmentsAsync(BaseItem item, IEnumerable? typeFilter, bool filterByProvider = true); + + /// + /// Gets information about any media segments stored for the given itemId. + /// + /// The id of the . + /// True if there are any segments stored for the item, otherwise false. + /// TODO: this should be async but as the only caller BaseItem.GetVersionInfo isn't async, this is also not. Venson. + bool HasSegments(Guid itemId); + + /// + /// Gets a list of all registered Segment Providers and their IDs. + /// + /// The media item that should be tested for providers. + /// A list of all providers for the tested item. + IEnumerable<(string Name, string Id)> GetSupportedProviders(BaseItem item); +} diff --git a/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs new file mode 100644 index 0000000000..39bb58bef2 --- /dev/null +++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model; +using MediaBrowser.Model.MediaSegments; + +namespace MediaBrowser.Controller; + +/// +/// Provides methods for Obtaining the Media Segments from an Item. +/// +public interface IMediaSegmentProvider +{ + /// + /// Gets the provider name. + /// + string Name { get; } + + /// + /// Enumerates all Media Segments from an Media Item. + /// + /// Arguments to enumerate MediaSegments. + /// Abort token. + /// A list of all MediaSegments found from this provider. + Task> GetMediaSegments(MediaSegmentGenerationRequest request, CancellationToken cancellationToken); + + /// + /// Should return support state for the given item. + /// + /// The base item to extract segments from. + /// True if item is supported, otherwise false. + ValueTask Supports(BaseItem item); +} diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs index bba5a6b851..bdc0f9a10f 100644 --- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs +++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs @@ -24,9 +24,9 @@ namespace MediaBrowser.Controller.Net DateTime LastActivityDate { get; } /// - /// Gets or sets the date of last Keeplive received. + /// Gets or sets the date of last Keepalive received. /// - /// The date of last Keeplive received. + /// The date of last Keepalive received. DateTime LastKeepAliveDate { get; set; } /// diff --git a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs index bd47db39a6..66a0c52547 100644 --- a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs +++ b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs @@ -5,7 +5,7 @@ using MediaBrowser.Controller.Library; namespace MediaBrowser.Controller.Sorting { /// - /// Represents a BaseItem comparer that requires a User to perform it's comparison. + /// Represents a BaseItem comparer that requires a User to perform its comparison. /// public interface IUserBaseItemComparer : IBaseItemComparer { diff --git a/MediaBrowser.Controller/Streaming/StreamState.cs b/MediaBrowser.Controller/Streaming/StreamState.cs index b5dbe29ec7..195dda5fe8 100644 --- a/MediaBrowser.Controller/Streaming/StreamState.cs +++ b/MediaBrowser.Controller/Streaming/StreamState.cs @@ -51,7 +51,7 @@ public class StreamState : EncodingJobInfo, IDisposable public VideoRequestDto? VideoRequest => Request as VideoRequestDto; /// - /// Gets or sets the direct stream provicer. + /// Gets or sets the direct stream provider. /// /// /// Deprecated. diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs index 9aa9c3548d..0bb341da18 100644 --- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs @@ -320,7 +320,7 @@ namespace MediaBrowser.LocalMetadata.Images { AddImage(files, images, name + "-fanart", ImageType.Backdrop, imagePrefix); - // Support without the prefix if it's in it's own folder + // Support without the prefix if it's in its own folder if (!isInMixedFolder) { AddImage(files, images, name + "-fanart", ImageType.Backdrop); diff --git a/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs index 952ed3aacb..00634de5b5 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs @@ -15,7 +15,7 @@ namespace MediaBrowser.LocalMetadata.Parsers /// /// Initializes a new instance of the class. /// - /// Instance of the interface. + /// Instance of the interface. /// Instance of the interface. public BoxSetXmlParser(ILogger logger, IProviderManager providerManager) : base(logger, providerManager) diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs index fca17d4c05..9b7e90b7af 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs @@ -84,7 +84,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo /// Gets the files matching a pattern. /// /// The search pattern. - /// All files of the directory matchign the search pattern. + /// All files of the directory matching the search pattern. public IFileInfo[] GetFiles(string searchPattern) { return _fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false) @@ -97,7 +97,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo /// /// The search pattern. /// The search optin. - /// All files of the directory matchign the search pattern and options. + /// All files of the directory matching the search pattern and options. public IFileInfo[] GetFiles(string searchPattern, SearchOption searchOption) { return _fileSystem.GetFiles( diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index e084bda27a..1eef181cb1 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -1101,14 +1101,14 @@ namespace MediaBrowser.MediaEncoding.Encoder private void StopProcesses() { - List proceses; + List processes; lock (_runningProcessesLock) { - proceses = _runningProcesses.ToList(); + processes = _runningProcesses.ToList(); _runningProcesses.Clear(); } - foreach (var process in proceses) + foreach (var process in processes) { if (!process.HasExited) { diff --git a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs index 1b5b5262a2..6f51e1a6ab 100644 --- a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs +++ b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs @@ -24,7 +24,7 @@ namespace MediaBrowser.MediaEncoding.Probing if (result.Streams is not null) { - // Convert all dictionaries to case insensitive + // Convert all dictionaries to case-insensitive foreach (var stream in result.Streams) { if (stream.Tags is not null) @@ -70,7 +70,7 @@ namespace MediaBrowser.MediaEncoding.Probing } /// - /// Converts a dictionary to case insensitive. + /// Converts a dictionary to case-insensitive. /// /// The dict. /// Dictionary{System.StringSystem.String}. diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 623a901c93..693bf90e71 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -83,9 +83,9 @@ public class ServerConfiguration : BaseApplicationConfiguration public bool QuickConnectAvailable { get; set; } = true; /// - /// Gets or sets a value indicating whether [enable case sensitive item ids]. + /// Gets or sets a value indicating whether [enable case-sensitive item ids]. /// - /// true if [enable case sensitive item ids]; otherwise, false. + /// true if [enable case-sensitive item ids]; otherwise, false. public bool EnableCaseSensitiveItemIds { get; set; } = true; public bool DisableLiveTvChannelUserDataName { get; set; } = true; @@ -249,7 +249,7 @@ public class ServerConfiguration : BaseApplicationConfiguration public bool AllowClientLogUpload { get; set; } = true; /// - /// Gets or sets the dummy chapter duration in seconds, use 0 (zero) or less to disable generation alltogether. + /// Gets or sets the dummy chapter duration in seconds, use 0 (zero) or less to disable generation altogether. /// /// The dummy chapters duration. public int DummyChapterDuration { get; set; } diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs index af0787990d..1b046f54ea 100644 --- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs +++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs @@ -25,8 +25,8 @@ namespace MediaBrowser.Model.Dlna /// The framerate. /// The packet length. /// The . - /// A value indicating whether tthe video is anamorphic. - /// A value indicating whether tthe video is interlaced. + /// A value indicating whether the video is anamorphic. + /// A value indicating whether the video is interlaced. /// The reference frames. /// The number of video streams. /// The number of audio streams. diff --git a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs index 438df34415..553ccfc64b 100644 --- a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs +++ b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs @@ -59,7 +59,7 @@ public class DirectPlayProfile /// True if supported. public bool SupportsAudioCodec(string? codec) { - // Video profiles can have audio codec restrictions too, therefore incude Video as valid type. + // Video profiles can have audio codec restrictions too, therefore include Video as valid type. return (Type == DlnaProfileType.Audio || Type == DlnaProfileType.Video) && ContainerHelper.ContainsContainer(AudioCodec, codec); } } diff --git a/MediaBrowser.Model/Entities/HardwareAccelerationType.cs b/MediaBrowser.Model/Entities/HardwareAccelerationType.cs index 198a2e00f6..ece18ec3e7 100644 --- a/MediaBrowser.Model/Entities/HardwareAccelerationType.cs +++ b/MediaBrowser.Model/Entities/HardwareAccelerationType.cs @@ -8,7 +8,7 @@ namespace MediaBrowser.Model.Entities; public enum HardwareAccelerationType { /// - /// Software accelleration. + /// Software acceleration. /// none = 0, diff --git a/MediaBrowser.Model/Entities/MetadataProvider.cs b/MediaBrowser.Model/Entities/MetadataProvider.cs index bd8db99416..dcc4ae88c5 100644 --- a/MediaBrowser.Model/Entities/MetadataProvider.cs +++ b/MediaBrowser.Model/Entities/MetadataProvider.cs @@ -27,7 +27,7 @@ namespace MediaBrowser.Model.Entities Tvdb = 4, /// - /// The tvcom providerd. + /// The tvcom provider. /// Tvcom = 5, diff --git a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs index 479ec7712d..385a86d31c 100644 --- a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs +++ b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs @@ -11,7 +11,7 @@ namespace MediaBrowser.Model.Entities; public static class ProviderIdsExtensions { /// - /// Case insensitive dictionary of string representation. + /// Case-insensitive dictionary of string representation. /// private static readonly Dictionary _metadataProviderEnumDictionary = Enum.GetValues() @@ -107,7 +107,7 @@ public static class ProviderIdsExtensions /// The instance. /// The name, this should not contain a '=' character. /// The value. - /// Due to how deserialization from the database works the name can not contain '='. + /// Due to how deserialization from the database works the name cannot contain '='. /// true if the provider id got set successfully; otherwise, false. public static bool TrySetProviderId(this IHasProviderIds instance, string? name, string? value) { @@ -153,7 +153,7 @@ public static class ProviderIdsExtensions /// The instance. /// The name, this should not contain a '=' character. /// The value. - /// Due to how deserialization from the database works the name can not contain '='. + /// Due to how deserialization from the database works the name cannot contain '='. public static void SetProviderId(this IHasProviderIds instance, string name, string value) { ArgumentNullException.ThrowIfNull(instance); diff --git a/MediaBrowser.Model/Globalization/ILocalizationManager.cs b/MediaBrowser.Model/Globalization/ILocalizationManager.cs index 02a29e7faf..20deaa5057 100644 --- a/MediaBrowser.Model/Globalization/ILocalizationManager.cs +++ b/MediaBrowser.Model/Globalization/ILocalizationManager.cs @@ -52,7 +52,7 @@ namespace MediaBrowser.Model.Globalization /// /// Gets the localization options. /// - /// . + /// . IEnumerable GetLocalizationOptions(); /// diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs index 2085328ddc..229368d004 100644 --- a/MediaBrowser.Model/IO/IFileSystem.cs +++ b/MediaBrowser.Model/IO/IFileSystem.cs @@ -145,7 +145,7 @@ namespace MediaBrowser.Model.IO /// Gets the directories. /// /// The path. - /// If set to true also searches in subdirectiories. + /// If set to true also searches in subdirectories. /// All found directories. IEnumerable GetDirectories(string path, bool recursive = false); @@ -153,7 +153,7 @@ namespace MediaBrowser.Model.IO /// Gets the files. /// /// The path in which to search. - /// If set to true also searches in subdirectiories. + /// If set to true also searches in subdirectories. /// All found files. IEnumerable GetFiles(string path, bool recursive = false); diff --git a/MediaBrowser.Model/Plugins/PluginStatus.cs b/MediaBrowser.Model/Plugins/PluginStatus.cs index bd420d7b4e..9c7a8f0c2c 100644 --- a/MediaBrowser.Model/Plugins/PluginStatus.cs +++ b/MediaBrowser.Model/Plugins/PluginStatus.cs @@ -34,7 +34,12 @@ namespace MediaBrowser.Model.Plugins Malfunctioned = -3, /// - /// This plugin has been superceded by another version. + /// This plugin has been superseded by another version. + /// + Superseded = -4, + + /// + /// [DEPRECATED] See Superseded. /// Superceded = -4, diff --git a/MediaBrowser.Model/Session/TranscodingInfo.cs b/MediaBrowser.Model/Session/TranscodingInfo.cs index ae25267aca..11e83844b0 100644 --- a/MediaBrowser.Model/Session/TranscodingInfo.cs +++ b/MediaBrowser.Model/Session/TranscodingInfo.cs @@ -5,7 +5,7 @@ using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.Session; /// -/// Class holding information on a runnning transcode. +/// Class holding information on a running transcode. /// public class TranscodingInfo { diff --git a/MediaBrowser.Model/System/PublicSystemInfo.cs b/MediaBrowser.Model/System/PublicSystemInfo.cs index 31a8956427..c26cfb667c 100644 --- a/MediaBrowser.Model/System/PublicSystemInfo.cs +++ b/MediaBrowser.Model/System/PublicSystemInfo.cs @@ -47,7 +47,7 @@ namespace MediaBrowser.Model.System /// Gets or sets a value indicating whether the startup wizard is completed. /// /// - /// Nullable for OpenAPI specification only to retain backwards compatibility in apiclients. + /// Nullable for OpenAPI specification only to retain backwards compatibility in api clients. /// /// The startup completion status.] public bool? StartupWizardCompleted { get; set; } diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 3ad8e1f69b..75ad0d58ca 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -73,7 +73,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers protected IProviderManager ProviderManager { get; } /// - /// Gets a value indicating whether URLs after a closing XML tag are supporrted. + /// Gets a value indicating whether URLs after a closing XML tag are supported. /// protected virtual bool SupportsUrlAfterClosingXmlTag => false; @@ -672,7 +672,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers } var fileSystemMetadata = _directoryService.GetFile(val); - // non existing file returns null + // nonexistent file returns null if (fileSystemMetadata is null || !fileSystemMetadata.Exists) { Logger.LogWarning("Artwork file {Path} specified in nfo file for {ItemName} does not exist.", uri, itemResult.Item.Name); diff --git a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs index 4cd676be12..df72ff0442 100644 --- a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs @@ -13,7 +13,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Savers { /// - /// Nfo saver for artsist. + /// Nfo saver for artist. /// public class ArtistNfoSaver : BaseNfoSaver { diff --git a/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs index c7a57859e8..790f60cf09 100644 --- a/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs @@ -138,7 +138,7 @@ namespace Jellyfin.LiveTv.Listings var programsInfo = new List(); foreach (ProgramDto schedule in dailySchedules.SelectMany(d => d.Programs)) { - // _logger.LogDebug("Proccesing Schedule for statio ID " + stationID + + // _logger.LogDebug("Processing Schedule for station ID " + stationID + // " which corresponds to channel " + channelNumber + " and program id " + // schedule.ProgramId + " which says it has images? " + // programDict[schedule.ProgramId].hasImageArtwork); diff --git a/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MapDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MapDto.cs index ea583a1cea..89c4ee5a89 100644 --- a/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MapDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MapDto.cs @@ -23,7 +23,7 @@ namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos /// Gets or sets the provider callsign. /// [JsonPropertyName("providerCallsign")] - public string? ProvderCallsign { get; set; } + public string? ProviderCallsign { get; set; } /// /// Gets or sets the logical channel number. diff --git a/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs index 8c3906f863..7bfc4bc8be 100644 --- a/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs @@ -64,7 +64,7 @@ namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos public IReadOnlyList Metadata { get; set; } = Array.Empty(); /// - /// Gets or sets the list of content raitings. + /// Gets or sets the list of content ratings. /// [JsonPropertyName("contentRating")] public IReadOnlyList ContentRating { get; set; } = Array.Empty(); diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index b1fc5d406c..3f71770b52 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -973,7 +973,7 @@ public class NetworkManager : INetworkManager, IDisposable bindPreference = string.Empty; int? port = null; - // Only consider subnets including the source IP, prefering specific overrides + // Only consider subnets including the source IP, preferring specific overrides List validPublishedServerUrls; if (!isInExternalSubnet) { diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs index 9fc0158235..d247b8cb18 100644 --- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs +++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs @@ -92,7 +92,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] }; - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,TotallyNotAVallidCommand,MoveDown"" }", _jsonOptions); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,TotallyNotAValidCommand,MoveDown"" }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } diff --git a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs index 69d20bd3fe..028f12afa7 100644 --- a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs +++ b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs @@ -6,8 +6,8 @@ namespace Jellyfin.Extensions.Tests public class StringExtensionsTests { [Theory] - [InlineData("", "")] // Identity edge-case (no diactritics) - [InlineData("Indiana Jones", "Indiana Jones")] // Identity (no diactritics) + [InlineData("", "")] // Identity edge-case (no diacritics) + [InlineData("Indiana Jones", "Indiana Jones")] // Identity (no diacritics) [InlineData("a\ud800b", "ab")] // Invalid UTF-16 char stripping [InlineData("åäö", "aao")] // Issue #7484 [InlineData("Jön", "Jon")] // Issue #7484 @@ -25,8 +25,8 @@ namespace Jellyfin.Extensions.Tests } [Theory] - [InlineData("", false)] // Identity edge-case (no diactritics) - [InlineData("Indiana Jones", false)] // Identity (no diactritics) + [InlineData("", false)] // Identity edge-case (no diacritics) + [InlineData("Indiana Jones", false)] // Identity (no diacritics) [InlineData("a\ud800b", true)] // Invalid UTF-16 char stripping [InlineData("åäö", true)] // Issue #7484 [InlineData("Jön", true)] // Issue #7484 diff --git a/tests/Jellyfin.LiveTv.Tests/SchedulesDirect/SchedulesDirectDeserializeTests.cs b/tests/Jellyfin.LiveTv.Tests/SchedulesDirect/SchedulesDirectDeserializeTests.cs index 6975d56d9e..59cd42c05b 100644 --- a/tests/Jellyfin.LiveTv.Tests/SchedulesDirect/SchedulesDirectDeserializeTests.cs +++ b/tests/Jellyfin.LiveTv.Tests/SchedulesDirect/SchedulesDirectDeserializeTests.cs @@ -232,7 +232,7 @@ namespace Jellyfin.LiveTv.Tests.SchedulesDirect Assert.Equal(2, channelDto!.Map.Count); Assert.Equal("24326", channelDto.Map[0].StationId); Assert.Equal("001", channelDto.Map[0].Channel); - Assert.Equal("BBC ONE South", channelDto.Map[0].ProvderCallsign); + Assert.Equal("BBC ONE South", channelDto.Map[0].ProviderCallsign); Assert.Equal("1", channelDto.Map[0].LogicalChannelNumber); Assert.Equal("providerCallsign", channelDto.Map[0].MatchType); } diff --git a/tests/Jellyfin.Naming.Tests/TV/TvParserHelpersTest.cs b/tests/Jellyfin.Naming.Tests/TV/TvParserHelpersTest.cs index 2d4b5b730e..5dd004408a 100644 --- a/tests/Jellyfin.Naming.Tests/TV/TvParserHelpersTest.cs +++ b/tests/Jellyfin.Naming.Tests/TV/TvParserHelpersTest.cs @@ -15,17 +15,17 @@ public class TvParserHelpersTest [InlineData("Unreleased", SeriesStatus.Unreleased)] public void SeriesStatusParserTest_Valid(string statusString, SeriesStatus? status) { - var successful = TvParserHelpers.TryParseSeriesStatus(statusString, out var parsered); + var successful = TvParserHelpers.TryParseSeriesStatus(statusString, out var parsed); Assert.True(successful); - Assert.Equal(status, parsered); + Assert.Equal(status, parsed); } [Theory] [InlineData("XXX")] public void SeriesStatusParserTest_InValid(string statusString) { - var successful = TvParserHelpers.TryParseSeriesStatus(statusString, out var parsered); + var successful = TvParserHelpers.TryParseSeriesStatus(statusString, out var parsed); Assert.False(successful); - Assert.Null(parsered); + Assert.Null(parsed); } } diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 3b7c43100f..4144300da0 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -238,7 +238,7 @@ namespace Jellyfin.Networking.Tests // User on external network, internal binding only - so assumption is a proxy forward, return external override. [InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "external=http://helloworld.com", "http://helloworld.com")] - // User on external network, no binding - so result is the 1st external which is overriden. + // User on external network, no binding - so result is the 1st external which is overridden. [InlineData("jellyfin.org", "192.168.1.0/24", "", false, "external=http://helloworld.com", "http://helloworld.com")] // User assumed to be internal, no binding - so result is the 1st matching interface. diff --git a/tests/Jellyfin.Providers.Tests/Omdb/JsonOmdbConverterTests.cs b/tests/Jellyfin.Providers.Tests/Omdb/JsonOmdbConverterTests.cs index eed9eedc78..3062cb7b42 100644 --- a/tests/Jellyfin.Providers.Tests/Omdb/JsonOmdbConverterTests.cs +++ b/tests/Jellyfin.Providers.Tests/Omdb/JsonOmdbConverterTests.cs @@ -31,7 +31,7 @@ namespace Jellyfin.Providers.Tests.Omdb [Theory] [InlineData("\"N/A\"")] [InlineData("null")] - public void Deserialization_To_Nullable_Int_Shoud_Be_Null(string input) + public void Deserialization_To_Nullable_Int_Should_Be_Null(string input) { var result = JsonSerializer.Deserialize(input, _options); Assert.Null(result); @@ -49,7 +49,7 @@ namespace Jellyfin.Providers.Tests.Omdb [Theory] [InlineData("\"N/A\"")] [InlineData("null")] - public void Deserialization_To_Nullable_String_Shoud_Be_Null(string input) + public void Deserialization_To_Nullable_String_Should_Be_Null(string input) { var result = JsonSerializer.Deserialize(input, _options); Assert.Null(result); diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest.json index 57367ce88c..6aa40c1dd9 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest.json +++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest.json @@ -540,7 +540,7 @@ { "guid": "022a3003-993f-45f1-8565-87d12af2e12a", "name": "InfuseSync", - "description": "This plugin will track all media changes while any Infuse clients are offline to decrease sync times when logging back in to your server.", + "description": "This plugin will track all media changes while any Infuse clients are offline to decrease sync times when logging back into your server.", "overview": "Blazing fast indexing for Infuse", "owner": "Firecore LLC", "category": "General", diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs index 39d449e27e..d92dbbd732 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs @@ -14,7 +14,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public sealed class DashboardControllerTests : IClassFixture { private readonly JellyfinApplicationFactory _factory; - private readonly JsonSerializerOptions _jsonOpions = JsonDefaults.Options; + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; private static string? _accessToken; public DashboardControllerTests(JellyfinApplicationFactory factory) @@ -65,7 +65,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(HttpStatusCode.OK, response.StatusCode); - _ = await response.Content.ReadFromJsonAsync(_jsonOpions); + _ = await response.Content.ReadFromJsonAsync(_jsonOptions); // TODO: check content } @@ -81,7 +81,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet); - var data = await response.Content.ReadFromJsonAsync(_jsonOpions); + var data = await response.Content.ReadFromJsonAsync(_jsonOptions); Assert.NotNull(data); Assert.Empty(data); } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs index 23de2489e5..64b9bd8e16 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs @@ -35,7 +35,7 @@ public sealed class ItemsControllerTests : IClassFixture CreateUserByName(HttpClient httpClient, CreateUserByName request) - => httpClient.PostAsJsonAsync("Users/New", request, _jsonOpions); + => httpClient.PostAsJsonAsync("Users/New", request, _jsonOptions); private Task UpdateUserPassword(HttpClient httpClient, Guid userId, UpdateUserPassword request) - => httpClient.PostAsJsonAsync("Users/" + userId.ToString("N", CultureInfo.InvariantCulture) + "/Password", request, _jsonOpions); + => httpClient.PostAsJsonAsync("Users/" + userId.ToString("N", CultureInfo.InvariantCulture) + "/Password", request, _jsonOptions); [Fact] [Priority(-1)] @@ -43,7 +43,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers using var response = await client.GetAsync("Users/Public"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var users = await response.Content.ReadFromJsonAsync(_jsonOpions); + var users = await response.Content.ReadFromJsonAsync(_jsonOptions); // User are hidden by default Assert.NotNull(users); Assert.Empty(users); @@ -58,7 +58,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers using var response = await client.GetAsync("Users"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var users = await response.Content.ReadFromJsonAsync(_jsonOpions); + var users = await response.Content.ReadFromJsonAsync(_jsonOptions); Assert.NotNull(users); Assert.Single(users); Assert.False(users![0].HasConfiguredPassword); @@ -90,7 +90,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers using var response = await CreateUserByName(client, createRequest); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var user = await response.Content.ReadFromJsonAsync(_jsonOpions); + var user = await response.Content.ReadFromJsonAsync(_jsonOptions); Assert.Equal(TestUsername, user!.Name); Assert.False(user.HasPassword); Assert.False(user.HasConfiguredPassword); @@ -151,7 +151,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); var users = await JsonSerializer.DeserializeAsync( - await client.GetStreamAsync("Users"), _jsonOpions); + await client.GetStreamAsync("Users"), _jsonOptions); var user = users!.First(x => x.Id.Equals(_testUserId)); Assert.True(user.HasPassword); Assert.True(user.HasConfiguredPassword); @@ -174,7 +174,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); var users = await JsonSerializer.DeserializeAsync( - await client.GetStreamAsync("Users"), _jsonOpions); + await client.GetStreamAsync("Users"), _jsonOptions); var user = users!.First(x => x.Id.Equals(_testUserId)); Assert.False(user.HasPassword); Assert.False(user.HasConfiguredPassword); diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs index 130281c6d2..8df86111ee 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs @@ -23,7 +23,7 @@ public sealed class UserLibraryControllerTests : IClassFixture x.Name)); Assert.Contains("Michael Green", writers.Select(x => x.Name)); - // Direcotrs + // Directors var directors = result.People.Where(x => x.Type == PersonKind.Director).ToArray(); Assert.Single(directors); Assert.Contains("David Slade", directors.Select(x => x.Name)); -- cgit v1.2.3 From 6fda268892871a977f8f18d198bc5f30d1b9b7ff Mon Sep 17 00:00:00 2001 From: luzpaz Date: Wed, 29 Jan 2025 10:56:25 -0500 Subject: Merge pull request #13453 from luzpaz/extentions-typo Fix source typo --- MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs index 1625c748a8..b085398c5e 100644 --- a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs @@ -22,7 +22,7 @@ namespace MediaBrowser.Controller.Entities.Audio IReadOnlyList Artists { get; set; } } - public static class Extentions + public static class Extensions { public static IEnumerable GetAllArtists(this T item) where T : IHasArtist, IHasAlbumArtist -- cgit v1.2.3