From d0b4b2ddb31a54f0705303ab8461be1125d66eab Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 7 Sep 2024 19:07:34 +0000 Subject: Migrated UserData from library sqlite db to jellyfin.db --- Emby.Server.Implementations/ApplicationHost.cs | 2 -- 1 file changed, 2 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 5292003f0..bdf013b5d 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -492,7 +492,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -580,7 +579,6 @@ namespace Emby.Server.Implementations } ((SqliteItemRepository)Resolve()).Initialize(); - ((SqliteUserDataRepository)Resolve()).Initialize(); var localizationManager = (LocalizationManager)Resolve(); await localizationManager.LoadAll().ConfigureAwait(false); -- 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 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index bdf013b5d..fbec4726f 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 000000000..14dc68a32 --- /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 eb55e32c5..ea7896861 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 dc83b763e..3839b1ae4 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 858465424..056d5b05e 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 0e67a7ca4..000000000 --- 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 000000000..92b5caf05 --- /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 6f8e1c39b..1fc721d6a 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 ad119d1c6..be353b5da 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 a3c0908bb..1063aaa8b 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 3b89ca62f..992f33ecf 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 014a0f1c9..8eb23f5e4 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 66cc765f3..000000000 --- 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 000000000..a3e617a21 --- /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 7b0f98fde..000000000 --- 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 000000000..d215a1d7a --- /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 288b1943e..000000000 --- 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 000000000..70c5ff1e2 --- /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 b7124283a..000000000 --- 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 000000000..f7b714c29 --- /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 d29d8b143..000000000 --- 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 000000000..3ced6e24e --- /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 fcc20a0d4..c1d6d58cd 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 c0f09670d..4aba9d07e 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 f34837c57..d15049a1f 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 a9e11f603..000000000 --- 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 55762c7fc..000000000 --- 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 000000000..e22cb0f58 --- /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 0d1e2a5a0..702ce39a2 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 eb605f6c8..a4764dd33 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 313b1459a..b27f156ef 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 000000000..1b2ca2acb --- /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 210d80afa..000000000 --- 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 000000000..4773f4058 --- /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 ec7c72935..000000000 --- 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 000000000..665129eaf --- /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 84e503fef..000000000 --- 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 000000000..43a24703e --- /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 246ba2733..62c590944 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 04da8fb88..f5e9dddcf 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 5d4732234..b57f2753f 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 22515ad6476a64e41f5d1e031a72d00c2c03ca7f Mon Sep 17 00:00:00 2001 From: JPVenson Date: Tue, 12 Nov 2024 17:23:41 +0000 Subject: Fixed app paths not being expanded --- Emby.Server.Implementations/ApplicationHost.cs | 5 ++ .../Item/BaseItemRepository.cs | 67 ++++------------------ .../Migrations/Routines/MigrateLibraryDb.cs | 2 +- 3 files changed, 16 insertions(+), 58 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index fbec4726f..372634340 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -268,6 +268,11 @@ namespace Emby.Server.Implementations public string ExpandVirtualPath(string path) { + if (path is null) + { + return null; + } + var appPaths = ApplicationPaths; return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index e89c43c45..8afe49f38 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1373,12 +1373,13 @@ public sealed class BaseItemRepository( /// /// The entity. /// The dto base instance. + /// The Application server Host. /// The dto to map. - public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto) + public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost) { dto.Id = entity.Id; dto.ParentId = entity.ParentId.GetValueOrDefault(); - dto.Path = entity.Path; + dto.Path = appHost?.ExpandVirtualPath(entity.Path) ?? entity.Path; dto.EndDate = entity.EndDate; dto.CommunityRating = entity.CommunityRating; dto.CustomRating = entity.CustomRating; @@ -1496,7 +1497,7 @@ public sealed class BaseItemRepository( if (entity.Images is not null) { - dto.ImageInfos = entity.Images.Select(Map).ToArray(); + dto.ImageInfos = entity.Images.Select(e => Map(e, appHost)).ToArray(); } // dto.Type = entity.Type; @@ -1727,6 +1728,7 @@ public sealed class BaseItemRepository( return BaseItemRepository.DeserialiseBaseItem( baseItemEntity, logger, + appHost, skipDeserialization || (serverConfigurationManager.Configuration.SkipDeserializationForBasicTypes && (typeToSerialise == typeof(Channel) || typeToSerialise == typeof(UserRootFolder)))); } @@ -1735,10 +1737,11 @@ public sealed class BaseItemRepository( /// /// The DB entity. /// Logger. + /// The application server Host. /// If only mapping should be processed. /// A mapped BaseItem. /// Will be thrown if an invalid serialisation is requested. - public static BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity, ILogger logger, bool skipDeserialization = false) + public static BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false) { var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialise unkown type."); BaseItemDto? dto = null; @@ -1760,7 +1763,7 @@ public sealed class BaseItemRepository( dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialise unkown type."); } - return Map(baseItemEntity, dto); + return Map(baseItemEntity, dto, appHost); } private QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetItemValues(InternalItemsQuery filter, ItemValueType[] itemValueTypes, string returnType) @@ -1909,51 +1912,6 @@ public sealed class BaseItemRepository( 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()); - } - } - } - private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e) { return new BaseItemImageInfo() @@ -1970,11 +1928,11 @@ public sealed class BaseItemRepository( }; } - private static ItemImageInfo Map(BaseItemImageInfo e) + private static ItemImageInfo Map(BaseItemImageInfo e, IServerApplicationHost? appHost) { return new ItemImageInfo() { - Path = e.Path, + Path = appHost?.ExpandVirtualPath(e.Path) ?? e.Path, BlurHash = e.Blurhash != null ? Encoding.UTF8.GetString(e.Blurhash) : null, DateModified = e.DateModified, Height = e.Height, @@ -1993,11 +1951,6 @@ public sealed class BaseItemRepository( return appHost.ReverseVirtualPath(path); } - private string RestorePath(string path) - { - return appHost.ExpandVirtualPath(path); - } - private List GetItemByNameTypesInQuery(InternalItemsQuery query) { var list = new List(); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index 2815b09ea..693c6c00c 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -1028,7 +1028,7 @@ public class MigrateLibraryDb : IMigrationRoutine entity.MediaType = mediaType; } - var baseItem = BaseItemRepository.DeserialiseBaseItem(entity, _logger, false); + var baseItem = BaseItemRepository.DeserialiseBaseItem(entity, _logger, null, false); var dataKeys = baseItem.GetUserDataKeys(); userDataKeys.AddRange(dataKeys); -- cgit v1.2.3 From ceb850c77052c465af8422dcf152f1d1d1530457 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Sat, 16 Nov 2024 18:11:01 +0100 Subject: Update projects to .NET 9 (#13023) --- .devcontainer/Dev - Server Ffmpeg/devcontainer.json | 6 +++--- .devcontainer/devcontainer.json | 6 +++--- .github/workflows/ci-codeql-analysis.yml | 2 +- .github/workflows/ci-openapi.yml | 8 ++++---- .github/workflows/ci-tests.yml | 2 +- .vscode/launch.json | 6 +++--- Directory.Build.props | 1 + Emby.Naming/Emby.Naming.csproj | 2 +- Emby.Photos/Emby.Photos.csproj | 2 +- Emby.Server.Implementations/ApplicationHost.cs | 2 +- Emby.Server.Implementations/Emby.Server.Implementations.csproj | 2 +- Emby.Server.Implementations/Library/UserDataManager.cs | 1 - Emby.Server.Implementations/Plugins/PluginManager.cs | 2 +- Jellyfin.Api/Controllers/ItemUpdateController.cs | 2 +- Jellyfin.Api/Jellyfin.Api.csproj | 2 +- Jellyfin.Data/Entities/User.cs | 2 +- Jellyfin.Data/Jellyfin.Data.csproj | 2 +- .../Jellyfin.Server.Implementations.csproj | 2 +- Jellyfin.Server/Helpers/StartupHelpers.cs | 8 -------- Jellyfin.Server/Jellyfin.Server.csproj | 2 +- MediaBrowser.Common/MediaBrowser.Common.csproj | 2 +- MediaBrowser.Common/Plugins/BasePluginOfT.cs | 2 +- MediaBrowser.Controller/MediaBrowser.Controller.csproj | 2 +- MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj | 2 +- MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj | 2 +- MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs | 2 +- MediaBrowser.Model/MediaBrowser.Model.csproj | 4 ++-- MediaBrowser.Providers/Manager/ProviderManager.cs | 3 +-- MediaBrowser.Providers/MediaBrowser.Providers.csproj | 2 +- MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj | 2 +- README.md | 4 ++-- .../Emby.Server.Implementations.Fuzz.csproj | 2 +- fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh | 2 +- fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj | 2 +- fuzz/Jellyfin.Api.Fuzz/fuzz.sh | 2 +- global.json | 2 +- jellyfin.ruleset | 4 ++++ src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj | 2 +- src/Jellyfin.Drawing/Jellyfin.Drawing.csproj | 2 +- src/Jellyfin.Extensions/Jellyfin.Extensions.csproj | 2 +- .../Json/Converters/JsonDelimitedArrayConverter.cs | 1 - src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj | 2 +- src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj | 2 +- .../Jellyfin.MediaEncoding.Keyframes.csproj | 2 +- src/Jellyfin.Networking/Jellyfin.Networking.csproj | 2 +- src/Jellyfin.Networking/Manager/NetworkManager.cs | 2 +- tests/Directory.Build.props | 2 +- .../Json/Models/GenericBodyArrayModel.cs | 2 +- .../Json/Models/GenericBodyIReadOnlyListModel.cs | 2 +- tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj | 4 ++-- .../TypedBaseItem/BaseItemKindTests.cs | 2 +- 51 files changed, 63 insertions(+), 69 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/.devcontainer/Dev - Server Ffmpeg/devcontainer.json b/.devcontainer/Dev - Server Ffmpeg/devcontainer.json index 0b848d9f3..a934512f4 100644 --- a/.devcontainer/Dev - Server Ffmpeg/devcontainer.json +++ b/.devcontainer/Dev - Server Ffmpeg/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Development Jellyfin Server - FFmpeg", - "image":"mcr.microsoft.com/devcontainers/dotnet:8.0-jammy", + "image":"mcr.microsoft.com/devcontainers/dotnet:9.0-jammy", // restores nuget packages, installs the dotnet workloads and installs the dev https certificate "postStartCommand": "dotnet restore; dotnet workload update; dotnet dev-certs https --trust; sudo bash \"./.devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh\"", // reads the extensions list and installs them @@ -8,8 +8,8 @@ "features": { "ghcr.io/devcontainers/features/dotnet:2": { "version": "none", - "dotnetRuntimeVersions": "8.0", - "aspNetCoreRuntimeVersions": "8.0" + "dotnetRuntimeVersions": "9.0", + "aspNetCoreRuntimeVersions": "9.0" }, "ghcr.io/devcontainers-contrib/features/apt-packages:1": { "preserve_apt_list": false, diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 063901c80..0cf768f1f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Development Jellyfin Server", - "image":"mcr.microsoft.com/devcontainers/dotnet:8.0-jammy", + "image":"mcr.microsoft.com/devcontainers/dotnet:9.0-jammy", // restores nuget packages, installs the dotnet workloads and installs the dev https certificate "postStartCommand": "dotnet restore; dotnet workload update; dotnet dev-certs https --trust", // reads the extensions list and installs them @@ -8,8 +8,8 @@ "features": { "ghcr.io/devcontainers/features/dotnet:2": { "version": "none", - "dotnetRuntimeVersions": "8.0", - "aspNetCoreRuntimeVersions": "8.0" + "dotnetRuntimeVersions": "9.0", + "aspNetCoreRuntimeVersions": "9.0" }, "ghcr.io/devcontainers-contrib/features/apt-packages:1": { "preserve_apt_list": false, diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index d6983c683..e6993d39d 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -24,7 +24,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 with: - dotnet-version: '8.0.x' + dotnet-version: '9.0.x' - name: Initialize CodeQL uses: github/codeql-action/init@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4 diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 4633461ad..353c47c54 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -23,7 +23,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 with: - dotnet-version: '8.0.x' + dotnet-version: '9.0.x' - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json @@ -32,7 +32,7 @@ jobs: name: openapi-head retention-days: 14 if-no-files-found: error - path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json + path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net9.0/openapi.json openapi-base: name: OpenAPI - BASE @@ -57,7 +57,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 with: - dotnet-version: '8.0.x' + dotnet-version: '9.0.x' - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json @@ -66,7 +66,7 @@ jobs: name: openapi-base retention-days: 14 if-no-files-found: error - path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json + path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net9.0/openapi.json openapi-diff: permissions: diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index aee3ebbf7..30aacc7a0 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -9,7 +9,7 @@ on: pull_request: env: - SDK_VERSION: "8.0.x" + SDK_VERSION: "9.0.x" jobs: run-tests: diff --git a/.vscode/launch.json b/.vscode/launch.json index 7e50d4f0a..d97d8de84 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll", + "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll", "args": [], "cwd": "${workspaceFolder}/Jellyfin.Server", "console": "internalConsole", @@ -22,7 +22,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll", + "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll", "args": ["--nowebclient"], "cwd": "${workspaceFolder}/Jellyfin.Server", "console": "internalConsole", @@ -34,7 +34,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll", + "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll", "args": ["--nowebclient", "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"], "cwd": "${workspaceFolder}/Jellyfin.Server", "console": "internalConsole", diff --git a/Directory.Build.props b/Directory.Build.props index 44a60ffb5..831188015 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,6 +8,7 @@ true + NU1902;NU1903 diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 53b297b06..20b32f3a6 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -6,7 +6,7 @@ - net8.0 + net9.0 false true true diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj index 55dbe393c..645a74aea 100644 --- a/Emby.Photos/Emby.Photos.csproj +++ b/Emby.Photos/Emby.Photos.csproj @@ -19,7 +19,7 @@ - net8.0 + net9.0 false true diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 5292003f0..13516896a 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -607,7 +607,7 @@ namespace Emby.Server.Implementations // Don't use an empty string password password = string.IsNullOrWhiteSpace(password) ? null : password; - var localCert = new X509Certificate2(path, password, X509KeyStorageFlags.UserKeySet); + var localCert = X509CertificateLoader.LoadPkcs12FromFile(path, password, X509KeyStorageFlags.UserKeySet); if (!localCert.HasPrivateKey) { Logger.LogError("No private key included in SSL cert {CertificateLocation}.", path); diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 34276355a..70dd5eb9a 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -37,7 +37,7 @@ - net8.0 + net9.0 false true diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index 62d22b23f..ceb3d65a4 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -84,7 +84,6 @@ namespace Emby.Server.Implementations.Library { ArgumentNullException.ThrowIfNull(user); ArgumentNullException.ThrowIfNull(item); - ArgumentNullException.ThrowIfNull(reason); ArgumentNullException.ThrowIfNull(userDataDto); var userData = GetUserData(user, item); diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index db82a2900..e7323d9d0 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -835,7 +835,7 @@ namespace Emby.Server.Implementations.Plugins /// If the is null. private bool TryGetPluginDlls(LocalPlugin plugin, out IReadOnlyList whitelistedDlls) { - ArgumentNullException.ThrowIfNull(nameof(plugin)); + ArgumentNullException.ThrowIfNull(plugin); IReadOnlyList pluginDlls = Directory.GetFiles(plugin.Path, "*.dll", SearchOption.AllDirectories); diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index 4001a6add..d49e0753e 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -457,7 +457,7 @@ public class ItemUpdateController : BaseJellyfinApiController return null; } - return (SeriesStatus)Enum.Parse(typeof(SeriesStatus), item.Status, true); + return Enum.Parse(item.Status, true); } private DateTime NormalizeDateTime(DateTime val) diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 5f86a6b6b..25feaa2d7 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -6,7 +6,7 @@ - net8.0 + net9.0 true diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs index 2c9cc8d78..9bbe9efe8 100644 --- a/Jellyfin.Data/Entities/User.cs +++ b/Jellyfin.Data/Entities/User.cs @@ -514,7 +514,7 @@ namespace Jellyfin.Data.Entities /// public void AddDefaultPreferences() { - foreach (var val in Enum.GetValues(typeof(PreferenceKind)).Cast()) + foreach (var val in Enum.GetValues()) { Preferences.Add(new Preference(val, string.Empty)); } diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 0c17d71e7..921cf2d8c 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 false true true diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index 20944ee4b..31cf24fb2 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 false true diff --git a/Jellyfin.Server/Helpers/StartupHelpers.cs b/Jellyfin.Server/Helpers/StartupHelpers.cs index 0802b23ad..bbf6d31f1 100644 --- a/Jellyfin.Server/Helpers/StartupHelpers.cs +++ b/Jellyfin.Server/Helpers/StartupHelpers.cs @@ -292,13 +292,5 @@ public static class StartupHelpers // Make sure we have all the code pages we can get // Ref: https://docs.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider.instance?view=netcore-3.0#remarks Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - - // Increase the max http request limit - // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others. - ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit); - - // Disable the "Expect: 100-Continue" header by default - // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c - ServicePointManager.Expect100Continue = false; } } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index e18212908..ebb12ba4e 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -8,7 +8,7 @@ jellyfin Exe - net8.0 + net9.0 false false true diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 51787d6a0..de6be4707 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -28,7 +28,7 @@ - net8.0 + net9.0 false true true diff --git a/MediaBrowser.Common/Plugins/BasePluginOfT.cs b/MediaBrowser.Common/Plugins/BasePluginOfT.cs index 116e9cef8..bf2f12cb9 100644 --- a/MediaBrowser.Common/Plugins/BasePluginOfT.cs +++ b/MediaBrowser.Common/Plugins/BasePluginOfT.cs @@ -193,7 +193,7 @@ namespace MediaBrowser.Common.Plugins } catch { - var config = (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType)); + var config = Activator.CreateInstance(); SaveConfiguration(config); return config; } diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 62f36bf28..ba4a2a59c 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -34,7 +34,7 @@ - net8.0 + net9.0 false true true diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj index 05177ac39..8e3c8cf7f 100644 --- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj +++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj @@ -11,7 +11,7 @@ - net8.0 + net9.0 false true diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index be63513a7..be7eeda92 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -6,7 +6,7 @@ - net8.0 + net9.0 false true diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 334796f58..c730f4cda 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -1648,7 +1648,7 @@ namespace MediaBrowser.MediaEncoding.Probing using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 1)) { - fs.Read(packetBuffer); + fs.ReadExactly(packetBuffer); } if (packetBuffer[0] == 71) diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index a3a575c0f..e9dab6bc8 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -14,7 +14,7 @@ - net8.0 + net9.0 false true true @@ -35,7 +35,7 @@ - + diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index c5689550d..e43da1350 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -262,7 +262,7 @@ namespace MediaBrowser.Providers.Manager try { var fileStream = AsyncFile.OpenRead(source); - await new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken); + await new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken).ConfigureAwait(false); } finally { @@ -1031,7 +1031,6 @@ namespace MediaBrowser.Providers.Manager /// public void QueueRefresh(Guid itemId, MetadataRefreshOptions options, RefreshPriority priority) { - ArgumentNullException.ThrowIfNull(itemId); if (itemId.IsEmpty()) { throw new ArgumentException("Guid can't be empty", nameof(itemId)); diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 9a65852f0..a3e0acf1b 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -28,7 +28,7 @@ - net8.0 + net9.0 false true ../jellyfin.ruleset diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj index c20073eea..b195af96c 100644 --- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj +++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj @@ -15,7 +15,7 @@ - net8.0 + net9.0 false true diff --git a/README.md b/README.md index 7da0cb30d..a07fd32ce 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ These instructions will help you get set up with a local development environment ### Prerequisites -Before the project can be built, you must first install the [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet) on your system. +Before the project can be built, you must first install the [.NET 9.0 SDK](https://dotnet.microsoft.com/download/dotnet) on your system. Instructions to run this project from the command line are included here, but you will also need to install an IDE if you want to debug the server while it is running. Any IDE that supports .NET 6 development will work, but two options are recent versions of [Visual Studio](https://visualstudio.microsoft.com/downloads/) (at least 2022) and [Visual Studio Code](https://code.visualstudio.com/Download). @@ -131,7 +131,7 @@ A second option is to build the project and then run the resulting executable fi ```bash dotnet build # Build the project -cd Jellyfin.Server/bin/Debug/net8.0 # Change into the build output directory +cd Jellyfin.Server/bin/Debug/net9.0 # Change into the build output directory ``` 2. Execute the build output. On Linux, Mac, etc. use `./jellyfin` and on Windows use `jellyfin.exe`. diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj index 73aae3f3d..1373d2fe0 100644 --- a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj +++ b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 diff --git a/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh b/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh index 80a5cd7c1..8183bb37a 100755 --- a/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh +++ b/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh @@ -8,4 +8,4 @@ cp bin/Emby.Server.Implementations.dll . dotnet build mkdir -p Findings -AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net8.0/Emby.Server.Implementations.Fuzz "$1" +AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net9.0/Emby.Server.Implementations.Fuzz "$1" diff --git a/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj b/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj index faac7d976..04c7be11d 100644 --- a/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj +++ b/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 diff --git a/fuzz/Jellyfin.Api.Fuzz/fuzz.sh b/fuzz/Jellyfin.Api.Fuzz/fuzz.sh index 96b0192cf..15148e1bb 100755 --- a/fuzz/Jellyfin.Api.Fuzz/fuzz.sh +++ b/fuzz/Jellyfin.Api.Fuzz/fuzz.sh @@ -8,4 +8,4 @@ cp bin/Jellyfin.Api.dll . dotnet build mkdir -p Findings -AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net8.0/Jellyfin.Api.Fuzz "$1" +AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net9.0/Jellyfin.Api.Fuzz "$1" diff --git a/global.json b/global.json index c9b932026..2e13a6387 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.404", + "version": "9.0.0", "rollForward": "latestMinor" } } diff --git a/jellyfin.ruleset b/jellyfin.ruleset index 67ffd9a37..ba04a70c2 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -154,6 +154,8 @@ + + @@ -168,6 +170,8 @@ + + diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index 0590ded32..ba402dfe0 100644 --- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -6,7 +6,7 @@ - net8.0 + net9.0 false true diff --git a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj index 4a02f90f9..5f4b3fe8d 100644 --- a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj +++ b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj @@ -6,7 +6,7 @@ - net8.0 + net9.0 false true diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index 1a42679fc..1613d83bc 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 false true true diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs index 936a5a97c..cdeaf29b0 100644 --- a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs +++ b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj b/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj index c58889740..f04c02504 100644 --- a/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj +++ b/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 true diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj index ee79802a1..dc581724a 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj +++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 true diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj index c79dcee3c..c826d3d9c 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj +++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 true diff --git a/src/Jellyfin.Networking/Jellyfin.Networking.csproj b/src/Jellyfin.Networking/Jellyfin.Networking.csproj index 24b3ecaab..472cdb7ef 100644 --- a/src/Jellyfin.Networking/Jellyfin.Networking.csproj +++ b/src/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 false true diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index 5a13cc417..10aed673b 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -57,7 +57,7 @@ public class NetworkManager : INetworkManager, IDisposable /// /// Dictionary containing interface addresses and their subnets. /// - private IReadOnlyList _interfaces; + private List _interfaces; /// /// Unfiltered user defined LAN subnets () diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index bec3481cb..146ad8dc2 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -4,7 +4,7 @@ - net8.0 + net9.0 false $(MSBuildThisFileDirectory)/jellyfin-tests.ruleset diff --git a/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyArrayModel.cs b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyArrayModel.cs index ef135278f..76669ea19 100644 --- a/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyArrayModel.cs +++ b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyArrayModel.cs @@ -8,7 +8,7 @@ namespace Jellyfin.Extensions.Tests.Json.Models /// The generic body model. /// /// The value type. - public class GenericBodyArrayModel + public sealed class GenericBodyArrayModel { /// /// Gets or sets the value. diff --git a/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyListModel.cs b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyListModel.cs index 8e7b5a35b..7e6b97afe 100644 --- a/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyListModel.cs +++ b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyListModel.cs @@ -8,7 +8,7 @@ namespace Jellyfin.Extensions.Tests.Json.Models /// The generic body IReadOnlyList model. /// /// The value type. - public class GenericBodyIReadOnlyListModel + public sealed class GenericBodyIReadOnlyListModel { /// /// Gets or sets the value. diff --git a/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj b/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj index cf967b84c..fdcf7d61e 100644 --- a/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj +++ b/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 @@ -22,7 +22,7 @@ - + diff --git a/tests/Jellyfin.Server.Implementations.Tests/TypedBaseItem/BaseItemKindTests.cs b/tests/Jellyfin.Server.Implementations.Tests/TypedBaseItem/BaseItemKindTests.cs index 1bd51b246..9a4389e7a 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/TypedBaseItem/BaseItemKindTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/TypedBaseItem/BaseItemKindTests.cs @@ -35,7 +35,7 @@ namespace Jellyfin.Server.Implementations.Tests.TypedBaseItem public void EnumParse_GivenValidBaseItemType_ReturnsEnumValue(Type baseItemDescendantType) { var enumValue = Enum.Parse(baseItemDescendantType.Name); - Assert.True(Enum.IsDefined(typeof(BaseItemKind), enumValue)); + Assert.True(Enum.IsDefined(enumValue)); } [Theory] -- cgit v1.2.3 From 25f8e2259a4d034a7fe14f357b26571b585b639d Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sun, 17 Nov 2024 14:14:14 +0000 Subject: Fixed ChapterRepository not set --- Emby.Server.Implementations/ApplicationHost.cs | 1 + 1 file changed, 1 insertion(+) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 5b99439f1..29967c6df 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -639,6 +639,7 @@ namespace Emby.Server.Implementations BaseItem.ProviderManager = Resolve(); BaseItem.LocalizationManager = Resolve(); BaseItem.ItemRepository = Resolve(); + BaseItem.ChapterRepository = Resolve(); BaseItem.FileSystem = Resolve(); BaseItem.UserDataManager = Resolve(); BaseItem.ChannelManager = Resolve(); -- cgit v1.2.3