diff options
Diffstat (limited to 'MediaBrowser.Controller')
96 files changed, 1606 insertions, 800 deletions
diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs index 81b532fda..976a667ac 100644 --- a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs +++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs @@ -1,7 +1,7 @@ #pragma warning disable CS1591 using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Model.Users; namespace MediaBrowser.Controller.Authentication diff --git a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs index 8c9d1baf8..592ce9955 100644 --- a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs +++ b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs @@ -4,7 +4,7 @@ using System; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Model.Users; namespace MediaBrowser.Controller.Authentication diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs index f186523b9..199e22b3f 100644 --- a/MediaBrowser.Controller/Channels/Channel.cs +++ b/MediaBrowser.Controller/Channels/Channel.cs @@ -7,8 +7,9 @@ using System.Globalization; using System.Linq; using System.Text.Json.Serialization; using System.Threading; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; +using Jellyfin.Data; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Querying; @@ -22,7 +23,7 @@ namespace MediaBrowser.Controller.Channels [JsonIgnore] public override SourceType SourceType => SourceType.Channel; - public override bool IsVisible(User user) + public override bool IsVisible(User user, bool skipAllowedTagsCheck = false) { var blockedChannelsPreference = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels); if (blockedChannelsPreference.Length != 0) @@ -41,7 +42,7 @@ namespace MediaBrowser.Controller.Channels } } - return base.IsVisible(user); + return base.IsVisible(user, skipAllowedTagsCheck); } protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) diff --git a/MediaBrowser.Controller/Chapters/IChapterManager.cs b/MediaBrowser.Controller/Chapters/IChapterManager.cs index c049bb97e..7532e56c6 100644 --- a/MediaBrowser.Controller/Chapters/IChapterManager.cs +++ b/MediaBrowser.Controller/Chapters/IChapterManager.cs @@ -1,19 +1,55 @@ using System; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -namespace MediaBrowser.Controller.Chapters +namespace MediaBrowser.Controller.Chapters; + +/// <summary> +/// Interface IChapterManager. +/// </summary> +public interface IChapterManager { /// <summary> - /// Interface IChapterManager. + /// Saves the chapters. + /// </summary> + /// <param name="video">The video.</param> + /// <param name="chapters">The set of chapters.</param> + void SaveChapters(Video video, IReadOnlyList<ChapterInfo> chapters); + + /// <summary> + /// Gets a single chapter of a BaseItem on a specific index. + /// </summary> + /// <param name="baseItemId">The BaseItems id.</param> + /// <param name="index">The index of that chapter.</param> + /// <returns>A chapter instance.</returns> + ChapterInfo? GetChapter(Guid baseItemId, int index); + + /// <summary> + /// Gets all chapters associated with the baseItem. + /// </summary> + /// <param name="baseItemId">The BaseItems id.</param> + /// <returns>A readonly list of chapter instances.</returns> + IReadOnlyList<ChapterInfo> GetChapters(Guid baseItemId); + + /// <summary> + /// Refreshes the chapter images. + /// </summary> + /// <param name="video">Video to use.</param> + /// <param name="directoryService">Directory service to use.</param> + /// <param name="chapters">Set of chapters to refresh.</param> + /// <param name="extractImages">Option to extract images.</param> + /// <param name="saveChapters">Option to save chapters.</param> + /// <param name="cancellationToken">CancellationToken to use for operation.</param> + /// <returns><c>true</c> if successful, <c>false</c> if not.</returns> + Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken); + + /// <summary> + /// Deletes the chapter images. /// </summary> - public interface IChapterManager - { - /// <summary> - /// Saves the chapters. - /// </summary> - /// <param name="itemId">The item.</param> - /// <param name="chapters">The set of chapters.</param> - void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters); - } + /// <param name="video">Video to use.</param> + void DeleteChapterImages(Video video); } diff --git a/MediaBrowser.Controller/Collections/ICollectionManager.cs b/MediaBrowser.Controller/Collections/ICollectionManager.cs index 38a78a67b..206b5ac42 100644 --- a/MediaBrowser.Controller/Collections/ICollectionManager.cs +++ b/MediaBrowser.Controller/Collections/ICollectionManager.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index cade53d99..ea38950d3 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -1,10 +1,10 @@ using System; using System.Threading.Tasks; using Jellyfin.Data.Dtos; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Events; using Jellyfin.Data.Queries; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Entities.Security; using MediaBrowser.Model.Devices; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; @@ -58,7 +58,7 @@ public interface IDeviceManager QueryResult<Device> GetDevices(DeviceQuery query); /// <summary> - /// Gets device infromation based on the provided query. + /// Gets device information based on the provided query. /// </summary> /// <param name="query">The device query.</param> /// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the device information.</returns> @@ -109,7 +109,7 @@ public interface IDeviceManager DeviceOptionsDto? GetDeviceOptions(string deviceId); /// <summary> - /// Gets the dto for client capabilites. + /// Gets the dto for client capabilities. /// </summary> /// <param name="capabilities">The client capabilities.</param> /// <returns><see cref="ClientCapabilitiesDto"/> of the device.</returns> diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index 0d1e2a5a0..4eeec99b0 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -3,9 +3,10 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Drawing @@ -60,11 +61,35 @@ namespace MediaBrowser.Controller.Drawing /// <summary> /// Gets the image cache tag. /// </summary> + /// <param name="baseItemPath">The items basePath.</param> + /// <param name="imageDateModified">The image last modification date.</param> + /// <returns>Guid.</returns> + string? GetImageCacheTag(string baseItemPath, DateTime imageDateModified); + + /// <summary> + /// Gets the image cache tag. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="image">The image.</param> + /// <returns>Guid.</returns> + string? GetImageCacheTag(BaseItemDto item, ChapterInfo image); + + /// <summary> + /// Gets the image cache tag. + /// </summary> /// <param name="item">The item.</param> /// <param name="image">The image.</param> /// <returns>Guid.</returns> string GetImageCacheTag(BaseItem item, ItemImageInfo image); + /// <summary> + /// Gets the image cache tag. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="image">The image.</param> + /// <returns>Guid.</returns> + string GetImageCacheTag(BaseItemDto item, ItemImageInfo image); + string? GetImageCacheTag(BaseItem item, ChapterInfo chapter); string? GetImageCacheTag(User user); diff --git a/MediaBrowser.Controller/Dto/DtoOptions.cs b/MediaBrowser.Controller/Dto/DtoOptions.cs index cb638cf90..a71cdbd62 100644 --- a/MediaBrowser.Controller/Dto/DtoOptions.cs +++ b/MediaBrowser.Controller/Dto/DtoOptions.cs @@ -34,7 +34,7 @@ namespace MediaBrowser.Controller.Dto EnableUserData = true; AddCurrentProgram = true; - Fields = allFields ? AllItemFields : Array.Empty<ItemFields>(); + Fields = allFields ? AllItemFields : []; ImageTypes = AllImageTypes; } diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs index 22453f0f7..f1d507fcb 100644 --- a/MediaBrowser.Controller/Dto/IDtoService.cs +++ b/MediaBrowser.Controller/Dto/IDtoService.cs @@ -1,7 +1,7 @@ #pragma warning disable CA1002 using System.Collections.Generic; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs index 5e0d1bb45..a02802f41 100644 --- a/MediaBrowser.Controller/Entities/AggregateFolder.cs +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -64,7 +64,7 @@ namespace MediaBrowser.Controller.Entities return CreateResolveArgs(directoryService, true).FileSystemChildren; } - protected override List<BaseItem> LoadChildren() + protected override IReadOnlyList<BaseItem> LoadChildren() { lock (_childIdsLock) { diff --git a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs index 1625c748a..b085398c5 100644 --- a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs @@ -22,7 +22,7 @@ namespace MediaBrowser.Controller.Entities.Audio IReadOnlyList<string> Artists { get; set; } } - public static class Extentions + public static class Extensions { public static IEnumerable<string> GetAllArtists<T>(this T item) where T : IHasArtist, IHasAlbumArtist diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index a0aae8769..d016d8f62 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -8,8 +8,10 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Data; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -21,6 +23,7 @@ namespace MediaBrowser.Controller.Entities.Audio /// <summary> /// Class MusicAlbum. /// </summary> + [Common.RequiresSourceSerialisation] public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo<AlbumInfo>, IMetadataContainer { public MusicAlbum() diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 1ab6c9706..58841e5b7 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -8,8 +8,10 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Data; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -21,6 +23,7 @@ namespace MediaBrowser.Controller.Entities.Audio /// <summary> /// Class MusicArtist. /// </summary> + [Common.RequiresSourceSerialisation] public class MusicArtist : Folder, IItemByName, IHasMusicGenres, IHasDualAccess, IHasLookupInfo<ArtistInfo> { [JsonIgnore] @@ -84,7 +87,7 @@ namespace MediaBrowser.Controller.Entities.Audio return !IsAccessedByName; } - public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query) { if (query.IncludeItemTypes.Length == 0) { @@ -110,15 +113,15 @@ namespace MediaBrowser.Controller.Entities.Audio return base.IsSaveLocalMetadataEnabled(); } - protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, bool allowRemoveRoot, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) + protected override async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, bool allowRemoveRoot, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { if (IsAccessedByName) { // Should never get in here anyway - return Task.CompletedTask; + return; } - return base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, false, refreshOptions, directoryService, cancellationToken); + await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, false, refreshOptions, directoryService, cancellationToken).ConfigureAwait(false); } public override List<string> GetUserDataKeys() @@ -137,11 +140,9 @@ namespace MediaBrowser.Controller.Entities.Audio private static List<string> GetUserDataKeys(MusicArtist item) { var list = new List<string>(); - var id = item.GetProviderId(MetadataProvider.MusicBrainzArtist); - - if (!string.IsNullOrEmpty(id)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out var externalId)) { - list.Add("Artist-Musicbrainz-" + id); + list.Add("Artist-Musicbrainz-" + externalId); } list.Add("Artist-" + (item.Name ?? string.Empty).RemoveDiacritics()); diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs index 7448d02ea..65669e680 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs @@ -14,6 +14,7 @@ namespace MediaBrowser.Controller.Entities.Audio /// <summary> /// Class MusicGenre. /// </summary> + [Common.RequiresSourceSerialisation] public class MusicGenre : BaseItem, IItemByName { [JsonIgnore] @@ -64,7 +65,7 @@ namespace MediaBrowser.Controller.Entities.Audio return true; } - public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query) { query.GenreIds = new[] { Id }; query.IncludeItemTypes = new[] { BaseItemKind.MusicVideo, BaseItemKind.Audio, BaseItemKind.MusicAlbum, BaseItemKind.MusicArtist }; diff --git a/MediaBrowser.Controller/Entities/AudioBook.cs b/MediaBrowser.Controller/Entities/AudioBook.cs index 782481fbc..666bf2a75 100644 --- a/MediaBrowser.Controller/Entities/AudioBook.cs +++ b/MediaBrowser.Controller/Entities/AudioBook.cs @@ -9,6 +9,7 @@ using MediaBrowser.Controller.Providers; namespace MediaBrowser.Controller.Entities { + [Common.RequiresSourceSerialisation] public class AudioBook : Audio.Audio, IHasSeries, IHasLookupInfo<SongInfo> { [JsonIgnore] diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index eb605f6c8..c2efa4ad3 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.IO; using System.Linq; @@ -11,15 +12,20 @@ using System.Text; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Data; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.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; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; @@ -29,7 +35,6 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Library; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Providers; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities @@ -47,7 +52,7 @@ namespace MediaBrowser.Controller.Entities /// The supported image extensions. /// </summary> public static readonly string[] SupportedImageExtensions - = new[] { ".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif", ".svg" }; + = [".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif", ".svg"]; private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions) { @@ -443,7 +448,7 @@ namespace MediaBrowser.Controller.Entities return Array.Empty<string>(); } - return new[] { Path }; + return [Path]; } } @@ -479,6 +484,8 @@ namespace MediaBrowser.Controller.Entities public static IItemRepository ItemRepository { get; set; } + public static IChapterManager ChapterManager { get; set; } + public static IFileSystem FileSystem { get; set; } public static IUserDataManager UserDataManager { get; set; } @@ -574,6 +581,9 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public int? InheritedParentalRatingValue { get; set; } + [JsonIgnore] + public int? InheritedParentalRatingSubValue { get; set; } + /// <summary> /// Gets or sets the critic rating. /// </summary> @@ -915,7 +925,7 @@ namespace MediaBrowser.Controller.Entities // Remove from middle if surrounded by spaces sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal); - // Remove from end if followed by a space + // Remove from end if preceeded by a space if (sortable.EndsWith(" " + search, StringComparison.Ordinal)) { sortable = sortable.Remove(sortable.Length - (search.Length + 1)); @@ -1041,7 +1051,7 @@ namespace MediaBrowser.Controller.Entities return PlayAccess.Full; } - public virtual List<MediaStream> GetMediaStreams() + public virtual IReadOnlyList<MediaStream> GetMediaStreams() { return MediaSourceManager.GetMediaStreams(new MediaStreamQuery { @@ -1054,7 +1064,7 @@ namespace MediaBrowser.Controller.Entities return false; } - public virtual List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution) + public virtual IReadOnlyList<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution) { if (SourceType == SourceType.Channel) { @@ -1088,7 +1098,7 @@ namespace MediaBrowser.Controller.Entities return 1; }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0) .ThenByDescending(i => i, new MediaSourceWidthComparator()) - .ToList(); + .ToArray(); } protected virtual IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources() @@ -1256,7 +1266,7 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Overrides the base implementation to refresh metadata for local trailers. + /// The base implementation to refresh metadata. /// </summary> /// <param name="options">The options.</param> /// <param name="cancellationToken">The cancellation token.</param> @@ -1299,7 +1309,7 @@ namespace MediaBrowser.Controller.Entities return false; } - if (GetParents().Any(i => !i.IsVisible(user))) + if (GetParents().Any(i => !i.IsVisible(user, true))) { return false; } @@ -1353,9 +1363,7 @@ namespace MediaBrowser.Controller.Entities protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService) { - var path = ContainingFolderPath; - - return directoryService.GetFileSystemEntries(path); + return directoryService.GetFileSystemEntries(ContainingFolderPath); } private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, IReadOnlyList<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) @@ -1384,6 +1392,23 @@ namespace MediaBrowser.Controller.Entities return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken); }); + // Cleanup removed extras + var removedExtraIds = item.ExtraIds.Where(e => !newExtraIds.Contains(e)).ToArray(); + if (removedExtraIds.Length > 0) + { + var removedExtras = LibraryManager.GetItemList(new InternalItemsQuery() + { + ItemIds = removedExtraIds + }); + foreach (var removedExtra in removedExtras) + { + LibraryManager.DeleteItem(removedExtra, new DeleteOptions() + { + DeleteFileLocation = false + }); + } + } + await Task.WhenAll(tasks).ConfigureAwait(false); item.ExtraIds = newExtraIds; @@ -1398,6 +1423,22 @@ namespace MediaBrowser.Controller.Entities public virtual bool RequiresRefresh() { + if (string.IsNullOrEmpty(Path) || DateModified == default) + { + return false; + } + + var info = FileSystem.GetFileSystemInfo(Path); + if (info.Exists) + { + if (info.IsDirectory) + { + return info.LastWriteTimeUtc != DateModified; + } + + return info.LastWriteTimeUtc != DateModified; + } + return false; } @@ -1521,18 +1562,20 @@ namespace MediaBrowser.Controller.Entities /// Determines if a given user has access to this item. /// </summary> /// <param name="user">The user.</param> + /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param> /// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns> /// <exception cref="ArgumentNullException">If user is null.</exception> - public bool IsParentalAllowed(User user) + public bool IsParentalAllowed(User user, bool skipAllowedTagsCheck) { ArgumentNullException.ThrowIfNull(user); - if (!IsVisibleViaTags(user)) + if (!IsVisibleViaTags(user, skipAllowedTagsCheck)) { return false; } - var maxAllowedRating = user.MaxParentalAgeRating; + var maxAllowedRating = user.MaxParentalRatingScore; + var maxAllowedSubRating = user.MaxParentalRatingSubScore; var rating = CustomRatingForComparison; if (string.IsNullOrEmpty(rating)) @@ -1546,10 +1589,10 @@ namespace MediaBrowser.Controller.Entities return !GetBlockUnratedValue(user); } - var value = LocalizationManager.GetRatingLevel(rating); + var ratingScore = LocalizationManager.GetRatingScore(rating); // Could not determine rating level - if (!value.HasValue) + if (ratingScore is null) { var isAllowed = !GetBlockUnratedValue(user); @@ -1561,10 +1604,15 @@ namespace MediaBrowser.Controller.Entities return isAllowed; } - return !maxAllowedRating.HasValue || value.Value <= maxAllowedRating.Value; + if (maxAllowedSubRating is not null) + { + return (ratingScore.SubScore ?? 0) <= maxAllowedSubRating && ratingScore.Score <= maxAllowedRating.Value; + } + + return !maxAllowedRating.HasValue || ratingScore.Score <= maxAllowedRating.Value; } - public int? GetInheritedParentalRatingValue() + public ParentalRatingScore GetParentalRatingScore() { var rating = CustomRatingForComparison; @@ -1578,7 +1626,7 @@ namespace MediaBrowser.Controller.Entities return null; } - return LocalizationManager.GetRatingLevel(rating); + return LocalizationManager.GetRatingScore(rating); } public List<string> GetInheritedTags() @@ -1599,7 +1647,7 @@ namespace MediaBrowser.Controller.Entities return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); } - private bool IsVisibleViaTags(User user) + private bool IsVisibleViaTags(User user, bool skipAllowedTagsCheck) { var allTags = GetInheritedTags(); if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase))) @@ -1614,7 +1662,7 @@ namespace MediaBrowser.Controller.Entities } var allowedTagsPreference = user.GetPreference(PreferenceKind.AllowedTags); - if (allowedTagsPreference.Length != 0 && !allowedTagsPreference.Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase))) + if (!skipAllowedTagsCheck && allowedTagsPreference.Length != 0 && !allowedTagsPreference.Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase))) { return false; } @@ -1654,13 +1702,14 @@ namespace MediaBrowser.Controller.Entities /// Default is just parental allowed. Can be overridden for more functionality. /// </summary> /// <param name="user">The user.</param> + /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param> /// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns> /// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception> - public virtual bool IsVisible(User user) + public virtual bool IsVisible(User user, bool skipAllowedTagsCheck = false) { ArgumentNullException.ThrowIfNull(user); - return IsParentalAllowed(user); + return IsParentalAllowed(user, skipAllowedTagsCheck); } public virtual bool IsVisibleStandalone(User user) @@ -1675,7 +1724,7 @@ namespace MediaBrowser.Controller.Entities public virtual string GetClientTypeName() { - if (IsFolder && SourceType == SourceType.Channel && this is not Channel) + if (IsFolder && SourceType == SourceType.Channel && this is not Channel && this is not Season && this is not Series) { return "ChannelFolderItem"; } @@ -1769,7 +1818,6 @@ namespace MediaBrowser.Controller.Entities public void AddStudio(string name) { ArgumentException.ThrowIfNullOrEmpty(name); - var current = Studios; if (!current.Contains(name, StringComparison.OrdinalIgnoreCase)) @@ -1781,21 +1829,21 @@ namespace MediaBrowser.Controller.Entities } else { - Studios = [..current, name]; + Studios = [.. current, name]; } } } public void SetStudios(IEnumerable<string> names) { - Studios = names.Distinct().ToArray(); + Studios = names.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } /// <summary> /// Adds a genre to the item. /// </summary> /// <param name="name">The name.</param> - /// <exception cref="ArgumentNullException">Throwns if name is null.</exception> + /// <exception cref="ArgumentNullException">Throws if name is null.</exception> public void AddGenre(string name) { ArgumentException.ThrowIfNullOrEmpty(name); @@ -1803,7 +1851,7 @@ namespace MediaBrowser.Controller.Entities var genres = Genres; if (!genres.Contains(name, StringComparison.OrdinalIgnoreCase)) { - Genres = [..genres, name]; + Genres = [.. genres, name]; } } @@ -1821,7 +1869,10 @@ namespace MediaBrowser.Controller.Entities { ArgumentNullException.ThrowIfNull(user); - var data = UserDataManager.GetUserData(user, this); + var data = UserDataManager.GetUserData(user, this) ?? new UserItemData() + { + Key = GetUserDataKeys().First(), + }; if (datePlayed.HasValue) { @@ -1964,7 +2015,7 @@ namespace MediaBrowser.Controller.Entities public void RemoveImage(ItemImageInfo image) { - RemoveImages(new[] { image }); + RemoveImages([image]); } public void RemoveImages(IEnumerable<ItemImageInfo> deletedImages) @@ -1974,11 +2025,11 @@ namespace MediaBrowser.Controller.Entities public void AddImage(ItemImageInfo image) { - ImageInfos = [..ImageInfos, image]; + ImageInfos = [.. ImageInfos, image]; } - public virtual Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken) - => LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken); + public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken) + => await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(false); /// <summary> /// Validates that images within the item are still on the filesystem. @@ -1999,7 +2050,7 @@ namespace MediaBrowser.Controller.Entities continue; } - (deletedImages ??= new List<ItemImageInfo>()).Add(imageInfo); + (deletedImages ??= []).Add(imageInfo); } var anyImagesRemoved = deletedImages?.Count > 0; @@ -2031,7 +2082,7 @@ namespace MediaBrowser.Controller.Entities { if (imageType == ImageType.Chapter) { - var chapter = ItemRepository.GetChapter(this, imageIndex); + var chapter = ChapterManager.GetChapter(Id, imageIndex); if (chapter is null) { @@ -2081,7 +2132,7 @@ namespace MediaBrowser.Controller.Entities if (image.Type == ImageType.Chapter) { - var chapters = ItemRepository.GetChapters(this); + var chapters = ChapterManager.GetChapters(Id); for (var i = 0; i < chapters.Count; i++) { if (chapters[i].ImagePath == image.Path) @@ -2202,11 +2253,7 @@ namespace MediaBrowser.Controller.Entities { return new[] { - new FileSystemMetadata - { - FullName = Path, - IsDirectory = IsFolder - } + FileSystem.GetFileSystemInfo(Path) }.Concat(GetLocalMetadataFilesToDelete()); } @@ -2214,7 +2261,7 @@ namespace MediaBrowser.Controller.Entities { if (IsFolder || !IsInMixedFolder) { - return new List<FileSystemMetadata>(); + return []; } var filename = System.IO.Path.GetFileNameWithoutExtension(Path); @@ -2367,7 +2414,7 @@ namespace MediaBrowser.Controller.Entities } } - protected Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOptions options, CancellationToken cancellationToken) + protected async Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOptions options, CancellationToken cancellationToken) { var newOptions = new MetadataRefreshOptions(options) { @@ -2428,10 +2475,10 @@ namespace MediaBrowser.Controller.Entities } } - return ownedItem.RefreshMetadata(newOptions, cancellationToken); + await ownedItem.RefreshMetadata(newOptions, cancellationToken).ConfigureAwait(false); } - protected Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken) + protected async Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken) { var newOptions = new MetadataRefreshOptions(options) { @@ -2441,9 +2488,7 @@ namespace MediaBrowser.Controller.Entities var id = LibraryManager.GetNewItemId(path, typeof(Video)); // Try to retrieve it from the db. If we don't find it, use the resolved version - var video = LibraryManager.GetItemById(id) as Video; - - if (video is null) + if (LibraryManager.GetItemById(id) is not Video video) { video = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Video; @@ -2452,15 +2497,15 @@ namespace MediaBrowser.Controller.Entities if (video is null) { - return Task.FromResult(true); + return; } if (video.OwnerId.IsEmpty()) { - video.OwnerId = this.Id; + video.OwnerId = Id; } - return RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken); + await RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken).ConfigureAwait(false); } public string GetEtag(User user) @@ -2472,10 +2517,10 @@ namespace MediaBrowser.Controller.Entities protected virtual List<string> GetEtagValues(User user) { - return new List<string> - { + return + [ DateLastSaved.Ticks.ToString(CultureInfo.InvariantCulture) - }; + ]; } public virtual IEnumerable<Guid> GetAncestorIds() @@ -2495,7 +2540,7 @@ namespace MediaBrowser.Controller.Entities public virtual IEnumerable<Guid> GetIdsForAncestorQuery() { - return new[] { Id }; + return [Id]; } public virtual double? GetRefreshProgress() @@ -2509,11 +2554,29 @@ namespace MediaBrowser.Controller.Entities var item = this; - var inheritedParentalRatingValue = item.GetInheritedParentalRatingValue() ?? null; - if (inheritedParentalRatingValue != item.InheritedParentalRatingValue) + var rating = item.GetParentalRatingScore(); + if (rating is not null) { - item.InheritedParentalRatingValue = inheritedParentalRatingValue; - updateType |= ItemUpdateType.MetadataImport; + if (rating.Score != item.InheritedParentalRatingValue) + { + item.InheritedParentalRatingValue = rating.Score; + updateType |= ItemUpdateType.MetadataImport; + } + + if (rating.SubScore != item.InheritedParentalRatingSubValue) + { + item.InheritedParentalRatingSubValue = rating.SubScore; + updateType |= ItemUpdateType.MetadataImport; + } + } + else + { + if (item.InheritedParentalRatingValue is not null) + { + item.InheritedParentalRatingValue = null; + item.InheritedParentalRatingSubValue = null; + updateType |= ItemUpdateType.MetadataImport; + } } return updateType; @@ -2524,7 +2587,7 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <param name="children">Media children.</param> /// <returns><c>true</c> if the rating was updated; otherwise <c>false</c>.</returns> - public bool UpdateRatingToItems(IList<BaseItem> children) + public bool UpdateRatingToItems(IReadOnlyList<BaseItem> children) { var currentOfficialRating = OfficialRating; @@ -2533,8 +2596,9 @@ namespace MediaBrowser.Controller.Entities .Select(i => i.OfficialRating) .Where(i => !string.IsNullOrEmpty(i)) .Distinct(StringComparer.OrdinalIgnoreCase) - .Select(rating => (rating, LocalizationManager.GetRatingLevel(rating))) - .OrderBy(i => i.Item2 ?? 1000) + .Select(rating => (rating, LocalizationManager.GetRatingScore(rating))) + .OrderBy(i => i.Item2 is null ? 1001 : i.Item2.Score) + .ThenBy(i => i.Item2 is null ? 1001 : i.Item2.SubScore) .Select(i => i.rating); OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating; diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs index 66dea1084..518766937 100644 --- a/MediaBrowser.Controller/Entities/Book.cs +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -10,6 +10,7 @@ using MediaBrowser.Controller.Providers; namespace MediaBrowser.Controller.Entities { + [Common.RequiresSourceSerialisation] public class Book : BaseItem, IHasLookupInfo<BookInfo>, IHasSeries { public Book() diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index 4ead477f8..ca79e6245 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -11,8 +11,8 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions.Json; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; @@ -96,11 +96,11 @@ namespace MediaBrowser.Controller.Entities return GetLibraryOptions(Path); } - public override bool IsVisible(User user) + public override bool IsVisible(User user, bool skipAllowedTagsCheck = false) { if (GetLibraryOptions().Enabled) { - return base.IsVisible(user); + return base.IsVisible(user, skipAllowedTagsCheck); } return false; diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 83c19a54e..e0c3b0a93 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; using System.Linq; using System.Security; @@ -11,8 +12,11 @@ using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; -using Jellyfin.Data.Entities; +using J2N.Collections.Generic.Extensions; +using Jellyfin.Data; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Collections; @@ -217,7 +221,7 @@ namespace MediaBrowser.Controller.Entities LibraryManager.CreateItem(item, this); } - public override bool IsVisible(User user) + public override bool IsVisible(User user, bool skipAllowedTagsCheck = false) { if (this is ICollectionFolder && this is not BasePluginFolder) { @@ -239,7 +243,7 @@ namespace MediaBrowser.Controller.Entities } } - return base.IsVisible(user); + return base.IsVisible(user, skipAllowedTagsCheck); } /// <summary> @@ -247,7 +251,7 @@ namespace MediaBrowser.Controller.Entities /// We want this synchronous. /// </summary> /// <returns>Returns children.</returns> - protected virtual List<BaseItem> LoadChildren() + protected virtual IReadOnlyList<BaseItem> LoadChildren() { // logger.LogDebug("Loading children from {0} {1} {2}", GetType().Name, Id, Path); // just load our children from the repo - the library will be validated and maintained in other processes @@ -528,13 +532,13 @@ namespace MediaBrowser.Controller.Entities } } - private Task RefreshMetadataRecursive(IList<BaseItem> children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken) + private async Task RefreshMetadataRecursive(IList<BaseItem> children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken) { - return RunTasks( + await RunTasks( (baseItem, innerProgress) => RefreshChildMetadata(baseItem, refreshOptions, recursive && baseItem.IsFolder, innerProgress, cancellationToken), children, progress, - cancellationToken); + cancellationToken).ConfigureAwait(false); } private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken) @@ -575,13 +579,13 @@ namespace MediaBrowser.Controller.Entities /// <param name="progress">The progress.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - private Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<double> progress, CancellationToken cancellationToken) + private async Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<double> progress, CancellationToken cancellationToken) { - return RunTasks( + await RunTasks( (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, true, false, false, null, directoryService, cancellationToken), children, progress, - cancellationToken); + cancellationToken).ConfigureAwait(false); } /// <summary> @@ -659,7 +663,7 @@ namespace MediaBrowser.Controller.Entities /// Get our children from the repo - stubbed for now. /// </summary> /// <returns>IEnumerable{BaseItem}.</returns> - protected List<BaseItem> GetCachedChildren() + protected IReadOnlyList<BaseItem> GetCachedChildren() { return ItemRepository.GetItemList(new InternalItemsQuery { @@ -1060,11 +1064,6 @@ namespace MediaBrowser.Controller.Entities return false; } - if (queryParent is Series) - { - return false; - } - if (queryParent is Season) { return false; @@ -1084,12 +1083,15 @@ namespace MediaBrowser.Controller.Entities if (!param.HasValue) { - if (user is not null && !configurationManager.Configuration.EnableGroupingIntoCollections) + if (user is not null && query.IncludeItemTypes.Any(type => + (type == BaseItemKind.Movie && !configurationManager.Configuration.EnableGroupingMoviesIntoCollections) || + (type == BaseItemKind.Series && !configurationManager.Configuration.EnableGroupingShowsIntoCollections))) { return false; } - if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Movie)) + if (query.IncludeItemTypes.Length == 0 + || query.IncludeItemTypes.Any(type => type == BaseItemKind.Movie || type == BaseItemKind.Series)) { param = true; } @@ -1200,22 +1202,22 @@ namespace MediaBrowser.Controller.Entities return false; } - if (request.IsHD.HasValue) + if (request.Is4K.HasValue) { return false; } - if (request.IsLocked.HasValue) + if (request.IsHD.HasValue) { return false; } - if (request.IsPlaceHolder.HasValue) + if (request.IsLocked.HasValue) { return false; } - if (request.IsPlayed.HasValue) + if (request.IsPlaceHolder.HasValue) { return false; } @@ -1240,11 +1242,6 @@ namespace MediaBrowser.Controller.Entities return false; } - if (request.GenreIds.Count > 0) - { - return false; - } - if (request.VideoTypes.Length > 0) { return false; @@ -1265,17 +1262,15 @@ namespace MediaBrowser.Controller.Entities return false; } - if (request.MinCommunityRating.HasValue) - { - return false; - } - - if (request.MinCriticRating.HasValue) + if (request.MinIndexNumber.HasValue) { return false; } - if (request.MinIndexNumber.HasValue) + if (request.OrderBy.Any(o => + o.OrderBy == ItemSortBy.CommunityRating || + o.OrderBy == ItemSortBy.CriticRating || + o.OrderBy == ItemSortBy.Runtime)) { return false; } @@ -1283,14 +1278,14 @@ namespace MediaBrowser.Controller.Entities return true; } - public List<BaseItem> GetChildren(User user, bool includeLinkedChildren) + public IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren) { ArgumentNullException.ThrowIfNull(user); return GetChildren(user, includeLinkedChildren, new InternalItemsQuery(user)); } - public virtual List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) { ArgumentNullException.ThrowIfNull(user); @@ -1304,7 +1299,7 @@ namespace MediaBrowser.Controller.Entities AddChildren(user, includeLinkedChildren, result, false, query); - return result.Values.ToList(); + return result.Values.ToArray(); } protected virtual IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) @@ -1369,7 +1364,7 @@ namespace MediaBrowser.Controller.Entities } } - public virtual IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) + public virtual IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) { ArgumentNullException.ThrowIfNull(user); @@ -1377,35 +1372,35 @@ namespace MediaBrowser.Controller.Entities AddChildren(user, true, result, true, query); - return result.Values; + return result.Values.ToArray(); } /// <summary> /// Gets the recursive children. /// </summary> /// <returns>IList{BaseItem}.</returns> - public IList<BaseItem> GetRecursiveChildren() + public IReadOnlyList<BaseItem> GetRecursiveChildren() { return GetRecursiveChildren(true); } - public IList<BaseItem> GetRecursiveChildren(bool includeLinkedChildren) + public IReadOnlyList<BaseItem> GetRecursiveChildren(bool includeLinkedChildren) { return GetRecursiveChildren(i => true, includeLinkedChildren); } - public IList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter) + public IReadOnlyList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter) { return GetRecursiveChildren(filter, true); } - public IList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter, bool includeLinkedChildren) + public IReadOnlyList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter, bool includeLinkedChildren) { var result = new Dictionary<Guid, BaseItem>(); AddChildrenToList(result, includeLinkedChildren, true, filter); - return result.Values.ToList(); + return result.Values.ToArray(); } /// <summary> @@ -1556,11 +1551,12 @@ namespace MediaBrowser.Controller.Entities /// Gets the linked children. /// </summary> /// <returns>IEnumerable{BaseItem}.</returns> - public IEnumerable<Tuple<LinkedChild, BaseItem>> GetLinkedChildrenInfos() + public IReadOnlyList<Tuple<LinkedChild, BaseItem>> GetLinkedChildrenInfos() { return LinkedChildren .Select(i => new Tuple<LinkedChild, BaseItem>(i, GetLinkedChild(i))) - .Where(i => i.Item2 is not null); + .Where(i => i.Item2 is not null) + .ToArray(); } protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs index ddf62dd4c..6ec78a270 100644 --- a/MediaBrowser.Controller/Entities/Genre.cs +++ b/MediaBrowser.Controller/Entities/Genre.cs @@ -14,6 +14,7 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Class Genre. /// </summary> + [Common.RequiresSourceSerialisation] public class Genre : BaseItem, IItemByName { /// <summary> @@ -61,7 +62,7 @@ namespace MediaBrowser.Controller.Entities return false; } - public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query) { query.GenreIds = new[] { Id }; query.ExcludeItemTypes = new[] diff --git a/MediaBrowser.Controller/Entities/IHasMediaSources.cs b/MediaBrowser.Controller/Entities/IHasMediaSources.cs index 90d9bdd2d..ad35494c2 100644 --- a/MediaBrowser.Controller/Entities/IHasMediaSources.cs +++ b/MediaBrowser.Controller/Entities/IHasMediaSources.cs @@ -22,8 +22,8 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <param name="enablePathSubstitution"><c>true</c> to enable path substitution, <c>false</c> to not.</param> /// <returns>A list of media sources.</returns> - List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution); + IReadOnlyList<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution); - List<MediaStream> GetMediaStreams(); + IReadOnlyList<MediaStream> GetMediaStreams(); } } diff --git a/MediaBrowser.Controller/Entities/IItemByName.cs b/MediaBrowser.Controller/Entities/IItemByName.cs index cac8aa61a..4928bda7a 100644 --- a/MediaBrowser.Controller/Entities/IItemByName.cs +++ b/MediaBrowser.Controller/Entities/IItemByName.cs @@ -9,7 +9,7 @@ namespace MediaBrowser.Controller.Entities /// </summary> public interface IItemByName { - IList<BaseItem> GetTaggedItems(InternalItemsQuery query); + IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query); } public interface IHasDualAccess : IItemByName diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 1461a3680..d50f3d075 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -3,8 +3,10 @@ using System; using System.Collections.Generic; using System.Linq; -using Jellyfin.Data.Entities; +using Jellyfin.Data; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Model.Entities; @@ -37,7 +39,6 @@ namespace MediaBrowser.Controller.Entities IncludeItemTypes = Array.Empty<BaseItemKind>(); ItemIds = Array.Empty<Guid>(); MediaTypes = Array.Empty<MediaType>(); - MinSimilarityScore = 20; OfficialRatings = Array.Empty<string>(); OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); PersonIds = Array.Empty<Guid>(); @@ -71,8 +72,6 @@ namespace MediaBrowser.Controller.Entities public User? User { get; set; } - public BaseItem? SimilarTo { get; set; } - public bool? IsFolder { get; set; } public bool? IsFavorite { get; set; } @@ -233,9 +232,9 @@ namespace MediaBrowser.Controller.Entities public int? IndexNumber { get; set; } - public int? MinParentalRating { get; set; } + public ParentalRatingScore? MinParentalRating { get; set; } - public int? MaxParentalRating { get; set; } + public ParentalRatingScore? MaxParentalRating { get; set; } public bool? HasDeadParentId { get; set; } @@ -295,8 +294,6 @@ namespace MediaBrowser.Controller.Entities public DtoOptions DtoOptions { get; set; } - public int MinSimilarityScore { get; set; } - public string? HasNoAudioTrackWithLanguage { get; set; } public string? HasNoInternalSubtitleTrackWithLanguage { get; set; } @@ -309,6 +306,8 @@ namespace MediaBrowser.Controller.Entities public bool? IsDeadStudio { get; set; } + public bool? IsDeadGenre { get; set; } + public bool? IsDeadPerson { get; set; } /// <summary> @@ -363,16 +362,17 @@ namespace MediaBrowser.Controller.Entities public void SetUser(User user) { - MaxParentalRating = user.MaxParentalAgeRating; - - if (MaxParentalRating.HasValue) + var maxRating = user.MaxParentalRatingScore; + if (maxRating.HasValue) { - string other = UnratedItem.Other.ToString(); - BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems) - .Where(i => i != other) - .Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray(); + MaxParentalRating = new(maxRating.Value, user.MaxParentalRatingSubScore); } + var other = UnratedItem.Other.ToString(); + BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems) + .Where(i => i != other) + .Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray(); + ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags); IncludeInheritedTags = user.GetPreference(PreferenceKind.AllowedTags); diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs index 3e1d89274..203a16a66 100644 --- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; namespace MediaBrowser.Controller.Entities { diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index a07187d2f..dd5852823 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -4,10 +4,13 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Text.Json.Serialization; -using Jellyfin.Data.Entities; +using Jellyfin.Data; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Querying; @@ -91,7 +94,7 @@ namespace MediaBrowser.Controller.Entities.Movies return Enumerable.Empty<BaseItem>(); } - protected override List<BaseItem> LoadChildren() + protected override IReadOnlyList<BaseItem> LoadChildren() { if (IsLegacyBoxSet) { @@ -99,7 +102,7 @@ namespace MediaBrowser.Controller.Entities.Movies } // Save a trip to the database - return new List<BaseItem>(); + return []; } public override bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders) @@ -127,16 +130,16 @@ namespace MediaBrowser.Controller.Entities.Movies return LibraryManager.Sort(items, user, new[] { sortBy }, SortOrder.Ascending); } - public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + public override IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) { var children = base.GetChildren(user, includeLinkedChildren, query); - return Sort(children, user).ToList(); + return Sort(children, user).ToArray(); } - public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) + public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) { var children = base.GetRecursiveChildren(user, query); - return Sort(children, user).ToList(); + return Sort(children, user).ToArray(); } public BoxSetInfo GetLookupInfo() @@ -144,14 +147,14 @@ namespace MediaBrowser.Controller.Entities.Movies return GetItemLookupInfo<BoxSetInfo>(); } - public override bool IsVisible(User user) + public override bool IsVisible(User user, bool skipAllowedTagsCheck = false) { if (IsLegacyBoxSet) { - return base.IsVisible(user); + return base.IsVisible(user, skipAllowedTagsCheck); } - if (base.IsVisible(user)) + if (base.IsVisible(user, skipAllowedTagsCheck)) { if (LinkedChildren.Length == 0) { @@ -194,7 +197,7 @@ namespace MediaBrowser.Controller.Entities.Movies var expandedFolders = new List<Guid>(); return FlattenItems(this, expandedFolders) - .SelectMany(i => LibraryManager.GetCollectionFolders(i)) + .SelectMany(LibraryManager.GetCollectionFolders) .Select(i => i.Id) .Distinct() .ToArray(); diff --git a/MediaBrowser.Controller/Entities/PeopleHelper.cs b/MediaBrowser.Controller/Entities/PeopleHelper.cs index 5292bd772..24b1843ce 100644 --- a/MediaBrowser.Controller/Entities/PeopleHelper.cs +++ b/MediaBrowser.Controller/Entities/PeopleHelper.cs @@ -10,11 +10,13 @@ namespace MediaBrowser.Controller.Entities { public static class PeopleHelper { - public static void AddPerson(List<PersonInfo> people, PersonInfo person) + public static void AddPerson(ICollection<PersonInfo> people, PersonInfo person) { ArgumentNullException.ThrowIfNull(person); ArgumentException.ThrowIfNullOrEmpty(person.Name); + person.Name = person.Name.Trim(); + // Normalize if (string.Equals(person.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase)) { diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs index 7f265084f..5cc4d322f 100644 --- a/MediaBrowser.Controller/Entities/Person.cs +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -14,6 +14,7 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// This is the full Person object that can be retrieved with all of it's data. /// </summary> + [Common.RequiresSourceSerialisation] public class Person : BaseItem, IItemByName, IHasLookupInfo<PersonLookupInfo> { /// <summary> @@ -62,7 +63,7 @@ namespace MediaBrowser.Controller.Entities return value; } - public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query) { query.PersonIds = new[] { Id }; diff --git a/MediaBrowser.Controller/Entities/PersonInfo.cs b/MediaBrowser.Controller/Entities/PersonInfo.cs index 3df0b0b78..0ed870bac 100644 --- a/MediaBrowser.Controller/Entities/PersonInfo.cs +++ b/MediaBrowser.Controller/Entities/PersonInfo.cs @@ -17,8 +17,14 @@ namespace MediaBrowser.Controller.Entities public PersonInfo() { ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + Id = Guid.NewGuid(); } + /// <summary> + /// Gets or Sets the PersonId. + /// </summary> + public Guid Id { get; set; } + public Guid ItemId { get; set; } /// <summary> diff --git a/MediaBrowser.Controller/Entities/PhotoAlbum.cs b/MediaBrowser.Controller/Entities/PhotoAlbum.cs index a7ecb9061..5b31b4f11 100644 --- a/MediaBrowser.Controller/Entities/PhotoAlbum.cs +++ b/MediaBrowser.Controller/Entities/PhotoAlbum.cs @@ -4,6 +4,7 @@ using System.Text.Json.Serialization; namespace MediaBrowser.Controller.Entities { + [Common.RequiresSourceSerialisation] public class PhotoAlbum : Folder { [JsonIgnore] diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs index a3736a4bf..9103b09a9 100644 --- a/MediaBrowser.Controller/Entities/Studio.cs +++ b/MediaBrowser.Controller/Entities/Studio.cs @@ -13,6 +13,7 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Class Studio. /// </summary> + [Common.RequiresSourceSerialisation] public class Studio : BaseItem, IItemByName { /// <summary> @@ -63,7 +64,7 @@ namespace MediaBrowser.Controller.Entities return true; } - public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query) { query.StudioIds = new[] { Id }; diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 46bad3f3b..6bdba36f9 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -7,12 +7,14 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.Json.Serialization; +using System.Threading; using Jellyfin.Data.Enums; using Jellyfin.Extensions; +using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Providers; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities.TV @@ -22,6 +24,8 @@ namespace MediaBrowser.Controller.Entities.TV /// </summary> public class Episode : Video, IHasTrailers, IHasLookupInfo<EpisodeInfo>, IHasSeries { + public static IMediaEncoder MediaEncoder { get; set; } + /// <inheritdoc /> [JsonIgnore] public IReadOnlyList<BaseItem> LocalTrailers => GetExtras() @@ -325,6 +329,39 @@ namespace MediaBrowser.Controller.Entities.TV { if (SourceType == SourceType.Library || SourceType == SourceType.LiveTV) { + var libraryOptions = LibraryManager.GetLibraryOptions(this); + if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(Container, "mp4", StringComparison.OrdinalIgnoreCase)) + { + try + { + var mediaInfo = MediaEncoder.GetMediaInfo( + new MediaInfoRequest + { + MediaSource = GetMediaSources(false)[0], + MediaType = DlnaProfileType.Video + }, + CancellationToken.None).GetAwaiter().GetResult(); + if (mediaInfo.ParentIndexNumber > 0) + { + ParentIndexNumber = mediaInfo.ParentIndexNumber; + } + + if (mediaInfo.IndexNumber > 0) + { + IndexNumber = mediaInfo.IndexNumber; + } + + if (!string.IsNullOrEmpty(mediaInfo.ShowName)) + { + SeriesName = mediaInfo.ShowName; + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Error reading the episode information with ffprobe. Episode: {EpisodeInfo}", Path); + } + } + try { if (LibraryManager.FillMissingEpisodeNumbersFromPath(this, replaceAllMetadata)) diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 181b9be2b..408161b03 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -7,9 +7,11 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.Json.Serialization; -using Jellyfin.Data.Entities; +using System.Threading; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; +using MediaBrowser.Common; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Querying; @@ -19,6 +21,7 @@ namespace MediaBrowser.Controller.Entities.TV /// <summary> /// Class Season. /// </summary> + [RequiresSourceSerialisation] public class Season : Folder, IHasSeries, IHasLookupInfo<SeasonInfo> { [JsonIgnore] @@ -132,7 +135,7 @@ namespace MediaBrowser.Controller.Entities.TV var series = Series; if (series is not null) { - return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000", CultureInfo.InvariantCulture); + return series.PresentationUniqueKey + "-" + IndexNumber.Value.ToString("000", CultureInfo.InvariantCulture); } } @@ -150,6 +153,21 @@ namespace MediaBrowser.Controller.Entities.TV protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) { + if (SourceType == SourceType.Channel) + { + try + { + query.Parent = this; + query.ChannelIds = new[] { ChannelId }; + return ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None).GetAwaiter().GetResult(); + } + catch + { + // Already logged at lower levels + return new QueryResult<BaseItem>(); + } + } + if (query.User is null) { return base.GetItemsInternal(query); @@ -255,7 +273,7 @@ namespace MediaBrowser.Controller.Entities.TV if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path)) { - IndexNumber ??= LibraryManager.GetSeasonNumberFromPath(Path); + IndexNumber ??= LibraryManager.GetSeasonNumberFromPath(Path, ParentId); // If a change was made record it if (IndexNumber.HasValue) diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 3e0e7b1c5..62c73d56f 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -9,12 +9,13 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Data; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Providers; using MediaBrowser.Model.Querying; using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider; @@ -23,7 +24,7 @@ namespace MediaBrowser.Controller.Entities.TV /// <summary> /// Class Series. /// </summary> - public class Series : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<SeriesInfo>, IMetadataContainer + public class Series : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<SeriesInfo>, IMetadataContainer, ISupportsBoxSetGrouping { public Series() { @@ -189,12 +190,12 @@ namespace MediaBrowser.Controller.Entities.TV return list; } - public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + public override IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) { return GetSeasons(user, new DtoOptions(true)); } - public List<BaseItem> GetSeasons(User user, DtoOptions options) + public IReadOnlyList<BaseItem> GetSeasons(User user, DtoOptions options) { var query = new InternalItemsQuery(user) { @@ -225,6 +226,21 @@ namespace MediaBrowser.Controller.Entities.TV { var user = query.User; + if (SourceType == SourceType.Channel) + { + try + { + query.Parent = this; + query.ChannelIds = [ChannelId]; + return ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None).GetAwaiter().GetResult(); + } + catch + { + // Already logged at lower levels + return new QueryResult<BaseItem>(); + } + } + if (query.Recursive) { var seriesKey = GetUniqueSeriesKey(this); @@ -367,7 +383,25 @@ namespace MediaBrowser.Controller.Entities.TV query.IsMissing = false; } - var allItems = LibraryManager.GetItemList(query); + IReadOnlyList<BaseItem> allItems; + if (SourceType == SourceType.Channel) + { + try + { + query.Parent = parentSeason; + query.ChannelIds = [ChannelId]; + allItems = [.. ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None).GetAwaiter().GetResult().Items]; + } + catch + { + // Already logged at lower levels + return []; + } + } + else + { + allItems = LibraryManager.GetItemList(query); + } return GetSeasonEpisodes(parentSeason, user, allItems, options, shouldIncludeMissingEpisodes); } diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs index 65d81b23e..bc7e22d9a 100644 --- a/MediaBrowser.Controller/Entities/UserRootFolder.cs +++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs @@ -8,7 +8,7 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; @@ -52,7 +52,7 @@ namespace MediaBrowser.Controller.Entities } } - protected override List<BaseItem> LoadChildren() + protected override IReadOnlyList<BaseItem> LoadChildren() { lock (_childIdsLock) { diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index e4fb340f7..dfa31315c 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -8,8 +8,8 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.TV; @@ -134,7 +134,7 @@ namespace MediaBrowser.Controller.Entities } /// <inheritdoc /> - public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) + public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) { query.SetUser(user); query.Recursive = true; @@ -145,7 +145,7 @@ namespace MediaBrowser.Controller.Entities } /// <inheritdoc /> - protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) + protected override IReadOnlyList<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) { return GetChildren(user, false); } diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 420349f35..1eb3c8f50 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -6,8 +6,10 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using Jellyfin.Data.Entities; +using Jellyfin.Data; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.TV; @@ -236,7 +238,7 @@ namespace MediaBrowser.Controller.Entities return ConvertToResult(_libraryManager.GetItemList(query)); } - private QueryResult<BaseItem> ConvertToResult(List<BaseItem> items) + private QueryResult<BaseItem> ConvertToResult(IReadOnlyList<BaseItem> items) { return new QueryResult<BaseItem>(items); } @@ -922,6 +924,11 @@ namespace MediaBrowser.Controller.Entities } } + if (query.ExcludeItemIds.Contains(item.Id)) + { + return false; + } + return true; } diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs index afdaf448b..37820296c 100644 --- a/MediaBrowser.Controller/Entities/Year.cs +++ b/MediaBrowser.Controller/Entities/Year.cs @@ -13,6 +13,7 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Class Year. /// </summary> + [Common.RequiresSourceSerialisation] public class Year : BaseItem, IItemByName { [JsonIgnore] @@ -55,7 +56,7 @@ namespace MediaBrowser.Controller.Entities return true; } - public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query) { if (!int.TryParse(Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year)) { diff --git a/MediaBrowser.Controller/IDisplayPreferencesManager.cs b/MediaBrowser.Controller/IDisplayPreferencesManager.cs index 10c0f56e0..a97096eae 100644 --- a/MediaBrowser.Controller/IDisplayPreferencesManager.cs +++ b/MediaBrowser.Controller/IDisplayPreferencesManager.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; namespace MediaBrowser.Controller { diff --git a/MediaBrowser.Controller/IO/IExternalDataManager.cs b/MediaBrowser.Controller/IO/IExternalDataManager.cs new file mode 100644 index 000000000..f69f4586c --- /dev/null +++ b/MediaBrowser.Controller/IO/IExternalDataManager.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.IO; + +/// <summary> +/// Interface IPathManager. +/// </summary> +public interface IExternalDataManager +{ + /// <summary> + /// Deletes all external item data. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task DeleteExternalItemDataAsync(BaseItem item, CancellationToken cancellationToken); +} diff --git a/MediaBrowser.Controller/IO/IPathManager.cs b/MediaBrowser.Controller/IO/IPathManager.cs new file mode 100644 index 000000000..eb6743754 --- /dev/null +++ b/MediaBrowser.Controller/IO/IPathManager.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.IO; + +/// <summary> +/// Interface IPathManager. +/// </summary> +public interface IPathManager +{ + /// <summary> + /// Gets the path to the trickplay image base folder. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="saveWithMedia">Whether or not the tile should be saved next to the media file.</param> + /// <returns>The absolute path.</returns> + public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false); + + /// <summary> + /// Gets the path to the subtitle file. + /// </summary> + /// <param name="mediaSourceId">The media source id.</param> + /// <param name="streamIndex">The stream index.</param> + /// <param name="extension">The subtitle file extension.</param> + /// <returns>The absolute path.</returns> + public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension); + + /// <summary> + /// Gets the path to the subtitle file. + /// </summary> + /// <param name="mediaSourceId">The media source id.</param> + /// <returns>The absolute path.</returns> + public string GetSubtitleFolderPath(string mediaSourceId); + + /// <summary> + /// Gets the path to the attachment file. + /// </summary> + /// <param name="mediaSourceId">The media source id.</param> + /// <param name="fileName">The attachmentFileName index.</param> + /// <returns>The absolute path.</returns> + public string GetAttachmentPath(string mediaSourceId, string fileName); + + /// <summary> + /// Gets the path to the attachment folder. + /// </summary> + /// <param name="mediaSourceId">The media source id.</param> + /// <returns>The absolute path.</returns> + public string GetAttachmentFolderPath(string mediaSourceId); + + /// <summary> + /// Gets the chapter images data path. + /// </summary> + /// <param name="item">The base item.</param> + /// <returns>The chapter images data path.</returns> + public string GetChapterImageFolderPath(BaseItem item); + + /// <summary> + /// Gets the chapter images path. + /// </summary> + /// <param name="item">The base item.</param> + /// <param name="chapterPositionTicks">The chapter position.</param> + /// <returns>The chapter images data path.</returns> + public string GetChapterImagePath(BaseItem item, long chapterPositionTicks); + + /// <summary> + /// Gets the paths of extracted data folders. + /// </summary> + /// <param name="item">The base item.</param> + /// <returns>The absolute paths.</returns> + public IReadOnlyList<string> GetExtractedDataPaths(BaseItem item); +} diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index e9c4d9e19..b76141db0 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -39,6 +39,11 @@ namespace MediaBrowser.Controller string FriendlyName { get; } /// <summary> + /// Gets or sets the path to the backup archive used to restore upon restart. + /// </summary> + string RestoreBackupPath { get; set; } + + /// <summary> /// Gets a URL specific for the request. /// </summary> /// <param name="request">The <see cref="HttpRequest"/> instance.</param> diff --git a/MediaBrowser.Controller/IServerApplicationPaths.cs b/MediaBrowser.Controller/IServerApplicationPaths.cs index 608286cd8..a6e83a02c 100644 --- a/MediaBrowser.Controller/IServerApplicationPaths.cs +++ b/MediaBrowser.Controller/IServerApplicationPaths.cs @@ -2,6 +2,10 @@ #pragma warning disable CS1591 +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; using MediaBrowser.Common.Configuration; namespace MediaBrowser.Controller diff --git a/MediaBrowser.Controller/ISystemManager.cs b/MediaBrowser.Controller/ISystemManager.cs index ef3034d2f..08344a1e5 100644 --- a/MediaBrowser.Controller/ISystemManager.cs +++ b/MediaBrowser.Controller/ISystemManager.cs @@ -1,5 +1,6 @@ using MediaBrowser.Model.System; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; namespace MediaBrowser.Controller; @@ -31,4 +32,10 @@ public interface ISystemManager /// Starts the application shutdown process. /// </summary> void Shutdown(); + + /// <summary> + /// Gets the systems storage resources. + /// </summary> + /// <returns>The <see cref="SystemStorageInfo"/>.</returns> + SystemStorageInfo GetSystemStorageInfo(); } diff --git a/MediaBrowser.Controller/Library/IIntroProvider.cs b/MediaBrowser.Controller/Library/IIntroProvider.cs index 4a9721acb..860e948af 100644 --- a/MediaBrowser.Controller/Library/IIntroProvider.cs +++ b/MediaBrowser.Controller/Library/IIntroProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Library @@ -23,6 +24,6 @@ namespace MediaBrowser.Controller.Library /// <param name="item">The item.</param> /// <param name="user">The user.</param> /// <returns>IEnumerable{System.String}.</returns> - Task<IEnumerable<IntroInfo>> GetIntros(BaseItem item, Jellyfin.Data.Entities.User user); + Task<IEnumerable<IntroInfo>> GetIntros(BaseItem item, User user); } } diff --git a/MediaBrowser.Controller/Library/IKeyframeManager.cs b/MediaBrowser.Controller/Library/IKeyframeManager.cs new file mode 100644 index 000000000..b0155efdd --- /dev/null +++ b/MediaBrowser.Controller/Library/IKeyframeManager.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.MediaEncoding.Keyframes; + +namespace MediaBrowser.Controller.IO; + +/// <summary> +/// Interface IKeyframeManager. +/// </summary> +public interface IKeyframeManager +{ + /// <summary> + /// Gets the keyframe data. + /// </summary> + /// <param name="itemId">The item id.</param> + /// <returns>The keyframe data.</returns> + IReadOnlyList<KeyframeData> GetKeyframeData(Guid itemId); + + /// <summary> + /// Saves the keyframe data. + /// </summary> + /// <param name="itemId">The item id.</param> + /// <param name="data">The keyframe data.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The task object representing the asynchronous operation.</returns> + Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken); + + /// <summary> + /// Deletes the keyframe data. + /// </summary> + /// <param name="itemId">The item id.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The task object representing the asynchronous operation.</returns> + Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken); +} diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index b802b7e6e..98ed15eb6 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -4,8 +4,9 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -219,13 +220,13 @@ namespace MediaBrowser.Controller.Library /// <param name="resolvers">The resolvers.</param> /// <param name="introProviders">The intro providers.</param> /// <param name="itemComparers">The item comparers.</param> - /// <param name="postscanTasks">The postscan tasks.</param> + /// <param name="postScanTasks">The post scan tasks.</param> void AddParts( IEnumerable<IResolverIgnoreRule> rules, IEnumerable<IItemResolver> resolvers, IEnumerable<IIntroProvider> introProviders, IEnumerable<IBaseItemComparer> itemComparers, - IEnumerable<ILibraryPostScanTask> postscanTasks); + IEnumerable<ILibraryPostScanTask> postScanTasks); /// <summary> /// Sorts the specified items. @@ -426,8 +427,9 @@ namespace MediaBrowser.Controller.Library /// Gets the season number from path. /// </summary> /// <param name="path">The path.</param> + /// <param name="parentId">The parent id.</param> /// <returns>System.Nullable<System.Int32>.</returns> - int? GetSeasonNumberFromPath(string path); + int? GetSeasonNumberFromPath(string path, Guid? parentId); /// <summary> /// Fills the missing episode numbers from path. @@ -483,21 +485,21 @@ namespace MediaBrowser.Controller.Library /// </summary> /// <param name="item">The item.</param> /// <returns>List<PersonInfo>.</returns> - List<PersonInfo> GetPeople(BaseItem item); + IReadOnlyList<PersonInfo> GetPeople(BaseItem item); /// <summary> /// Gets the people. /// </summary> /// <param name="query">The query.</param> /// <returns>List<PersonInfo>.</returns> - List<PersonInfo> GetPeople(InternalPeopleQuery query); + IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query); /// <summary> /// Gets the people items. /// </summary> /// <param name="query">The query.</param> /// <returns>List<Person>.</returns> - List<Person> GetPeopleItems(InternalPeopleQuery query); + IReadOnlyList<Person> GetPeopleItems(InternalPeopleQuery query); /// <summary> /// Updates the people. @@ -513,21 +515,21 @@ namespace MediaBrowser.Controller.Library /// <param name="people">The people.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The async task.</returns> - Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken); + Task UpdatePeopleAsync(BaseItem item, IReadOnlyList<PersonInfo> people, CancellationToken cancellationToken); /// <summary> /// Gets the item ids. /// </summary> /// <param name="query">The query.</param> /// <returns>List<Guid>.</returns> - List<Guid> GetItemIds(InternalItemsQuery query); + IReadOnlyList<Guid> GetItemIds(InternalItemsQuery query); /// <summary> /// Gets the people names. /// </summary> /// <param name="query">The query.</param> /// <returns>List<System.String>.</returns> - List<string> GetPeopleNames(InternalPeopleQuery query); + IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query); /// <summary> /// Queries the items. @@ -553,9 +555,9 @@ namespace MediaBrowser.Controller.Library /// </summary> /// <param name="query">The query.</param> /// <returns>QueryResult<BaseItem>.</returns> - List<BaseItem> GetItemList(InternalItemsQuery query); + IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query); - List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent); + IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent); /// <summary> /// Gets the items. @@ -563,7 +565,25 @@ namespace MediaBrowser.Controller.Library /// <param name="query">The query to use.</param> /// <param name="parents">Items to use for query.</param> /// <returns>List of items.</returns> - List<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents); + IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents); + + /// <summary> + /// Gets the TVShow/Album items for Latest api. + /// </summary> + /// <param name="query">The query to use.</param> + /// <param name="parents">Items to use for query.</param> + /// <param name="collectionType">Collection Type.</param> + /// <returns>List of items.</returns> + IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery query, IReadOnlyList<BaseItem> parents, CollectionType collectionType); + + /// <summary> + /// Gets the list of series presentation keys for next up. + /// </summary> + /// <param name="query">The query to use.</param> + /// <param name="parents">Items to use for query.</param> + /// <param name="dateCutoff">The minimum date for a series to have been most recently watched.</param> + /// <returns>List of series presentation keys.</returns> + IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents, DateTime dateCutoff); /// <summary> /// Gets the items result. @@ -573,11 +593,11 @@ namespace MediaBrowser.Controller.Library QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query); /// <summary> - /// Ignores the file. + /// Checks if the file is ignored. /// </summary> /// <param name="file">The file.</param> /// <param name="parent">The parent.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> + /// <returns><c>true</c> if ignored, <c>false</c> otherwise.</returns> bool IgnoreFile(FileSystemMetadata file, BaseItem parent); Guid GetStudioId(string name); diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs index 44a1a85e3..2b6781a19 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs @@ -6,7 +6,7 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; @@ -29,31 +29,31 @@ namespace MediaBrowser.Controller.Library /// </summary> /// <param name="itemId">The item identifier.</param> /// <returns>IEnumerable<MediaStream>.</returns> - List<MediaStream> GetMediaStreams(Guid itemId); + IReadOnlyList<MediaStream> GetMediaStreams(Guid itemId); /// <summary> /// Gets the media streams. /// </summary> /// <param name="query">The query.</param> /// <returns>IEnumerable<MediaStream>.</returns> - List<MediaStream> GetMediaStreams(MediaStreamQuery query); + IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery query); /// <summary> /// Gets the media attachments. /// </summary> /// <param name="itemId">The item identifier.</param> /// <returns>IEnumerable<MediaAttachment>.</returns> - List<MediaAttachment> GetMediaAttachments(Guid itemId); + IReadOnlyList<MediaAttachment> GetMediaAttachments(Guid itemId); /// <summary> /// Gets the media attachments. /// </summary> /// <param name="query">The query.</param> /// <returns>IEnumerable<MediaAttachment>.</returns> - List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query); + IReadOnlyList<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query); /// <summary> - /// Gets the playack media sources. + /// Gets the playback media sources. /// </summary> /// <param name="item">Item to use.</param> /// <param name="user">User to use for operation.</param> @@ -61,7 +61,7 @@ namespace MediaBrowser.Controller.Library /// <param name="enablePathSubstitution">Option to enable path substitution.</param> /// <param name="cancellationToken">CancellationToken to use for operation.</param> /// <returns>List of media sources wrapped in an awaitable task.</returns> - Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken); + Task<IReadOnlyList<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken); /// <summary> /// Gets the static media sources. @@ -70,7 +70,7 @@ namespace MediaBrowser.Controller.Library /// <param name="enablePathSubstitution">Option to enable path substitution.</param> /// <param name="user">User to use for operation.</param> /// <returns>List of media sources.</returns> - List<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null); + IReadOnlyList<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null); /// <summary> /// Gets the static media source. @@ -123,7 +123,7 @@ namespace MediaBrowser.Controller.Library /// <param name="info">The <see cref="ActiveRecordingInfo"/>.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param> /// <returns>A task containing the <see cref="MediaSourceInfo"/>'s for the recording.</returns> - Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken); + Task<IReadOnlyList<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken); /// <summary> /// Closes the media source. diff --git a/MediaBrowser.Controller/Library/IMusicManager.cs b/MediaBrowser.Controller/Library/IMusicManager.cs index 93073cc79..20764ec60 100644 --- a/MediaBrowser.Controller/Library/IMusicManager.cs +++ b/MediaBrowser.Controller/Library/IMusicManager.cs @@ -1,7 +1,7 @@ #pragma warning disable CA1002, CS1591 using System.Collections.Generic; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -17,7 +17,7 @@ namespace MediaBrowser.Controller.Library /// <param name="user">The user to use.</param> /// <param name="dtoOptions">The options to use.</param> /// <returns>List of items.</returns> - List<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions); + IReadOnlyList<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions); /// <summary> /// Gets the instant mix from artist. @@ -26,7 +26,7 @@ namespace MediaBrowser.Controller.Library /// <param name="user">The user to use.</param> /// <param name="dtoOptions">The options to use.</param> /// <returns>List of items.</returns> - List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions); + IReadOnlyList<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions); /// <summary> /// Gets the instant mix from genre. @@ -35,6 +35,6 @@ namespace MediaBrowser.Controller.Library /// <param name="user">The user to use.</param> /// <param name="dtoOptions">The options to use.</param> /// <returns>List of items.</returns> - List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions); + IReadOnlyList<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions); } } diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs index f36fd393f..eb46611dd 100644 --- a/MediaBrowser.Controller/Library/IUserDataManager.cs +++ b/MediaBrowser.Controller/Library/IUserDataManager.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Threading; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; @@ -44,7 +44,7 @@ namespace MediaBrowser.Controller.Library /// <param name="user">User to use.</param> /// <param name="item">Item to use.</param> /// <returns>User data.</returns> - UserItemData GetUserData(User user, BaseItem item); + UserItemData? GetUserData(User user, BaseItem item); /// <summary> /// Gets the user data dto. @@ -52,7 +52,7 @@ namespace MediaBrowser.Controller.Library /// <param name="item">Item to use.</param> /// <param name="user">User to use.</param> /// <returns>User data dto.</returns> - UserItemDataDto GetUserDataDto(BaseItem item, User user); + UserItemDataDto? GetUserDataDto(BaseItem item, User user); /// <summary> /// Gets the user data dto. @@ -62,7 +62,7 @@ namespace MediaBrowser.Controller.Library /// <param name="user">User to use.</param> /// <param name="options">Dto options to use.</param> /// <returns>User data dto.</returns> - UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options); + UserItemDataDto? GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options); /// <summary> /// Updates playstate for an item and returns true or false indicating if it was played to completion. diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index 1c115be85..0109cf4b7 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -3,8 +3,8 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Events; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Users; diff --git a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs index 76e9eb1f5..b0a6782c7 100644 --- a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs +++ b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index c0e46ba24..8d59eef9f 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -6,8 +6,8 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Events; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs index 3c2cf8e3d..b10e77e10 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Text.Json.Serialization; @@ -119,13 +120,10 @@ namespace MediaBrowser.Controller.LiveTv return "TvChannel"; } - public IEnumerable<BaseItem> GetTaggedItems() - => Enumerable.Empty<BaseItem>(); + public IEnumerable<BaseItem> GetTaggedItems() => []; - public override List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution) + public override IReadOnlyList<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution) { - var list = new List<MediaSourceInfo>(); - var info = new MediaSourceInfo { Id = Id.ToString("N", CultureInfo.InvariantCulture), @@ -138,14 +136,12 @@ namespace MediaBrowser.Controller.LiveTv IsInfiniteStream = RunTimeTicks is null }; - list.Add(info); - - return list; + return [info]; } - public override List<MediaStream> GetMediaStreams() + public override IReadOnlyList<MediaStream> GetMediaStreams() { - return new List<MediaStream>(); + return []; } protected override string GetInternalMetadataPath(string basePath) diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs index 2ac6f9963..83944f741 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs @@ -18,6 +18,7 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Controller.LiveTv { + [Common.RequiresSourceSerialisation] public class LiveTvProgram : BaseItem, IHasLookupInfo<ItemLookupInfo>, IHasStartDate, IHasProgramAttributes { private const string EmbyServiceName = "Emby"; diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index ba4a2a59c..3353ad63f 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -18,6 +18,7 @@ </PropertyGroup> <ItemGroup> + <PackageReference Include="BitFaster.Caching" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" /> <PackageReference Include="System.Threading.Tasks.Dataflow" /> @@ -27,6 +28,7 @@ <ProjectReference Include="../Emby.Naming/Emby.Naming.csproj" /> <ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" /> <ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" /> + <ProjectReference Include="../src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.Controller/MediaEncoding/BitStreamFilterOptionType.cs b/MediaBrowser.Controller/MediaEncoding/BitStreamFilterOptionType.cs new file mode 100644 index 000000000..41d21e440 --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/BitStreamFilterOptionType.cs @@ -0,0 +1,32 @@ +namespace MediaBrowser.Controller.MediaEncoding; + +/// <summary> +/// Enum BitStreamFilterOptionType. +/// </summary> +public enum BitStreamFilterOptionType +{ + /// <summary> + /// hevc_metadata bsf with remove_dovi option. + /// </summary> + HevcMetadataRemoveDovi = 0, + + /// <summary> + /// hevc_metadata bsf with remove_hdr10plus option. + /// </summary> + HevcMetadataRemoveHdr10Plus = 1, + + /// <summary> + /// av1_metadata bsf with remove_dovi option. + /// </summary> + Av1MetadataRemoveDovi = 2, + + /// <summary> + /// av1_metadata bsf with remove_hdr10plus option. + /// </summary> + Av1MetadataRemoveHdr10Plus = 3, + + /// <summary> + /// dovi_rpu bsf with strip option. + /// </summary> + DoviRpuStrip = 4, +} diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 9399679a4..364470cd2 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -13,10 +13,13 @@ using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using System.Threading; +using Jellyfin.Data; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Extensions; +using MediaBrowser.Controller.IO; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; @@ -35,7 +38,13 @@ namespace MediaBrowser.Controller.MediaEncoding /// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters. /// This should matches all common valid codecs. /// </summary> - public const string ValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$"; + public const string ContainerValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$"; + + /// <summary> + /// The level validation regex. + /// This regular expression matches strings representing a double. + /// </summary> + public const string LevelValidationRegex = @"-?[0-9]+(?:\.[0-9]+)?"; private const string _defaultMjpegEncoder = "mjpeg"; @@ -53,6 +62,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly ISubtitleEncoder _subtitleEncoder; private readonly IConfiguration _config; private readonly IConfigurationManager _configurationManager; + private readonly IPathManager _pathManager; // i915 hang was fixed by linux 6.2 (3f882f2) private readonly Version _minKerneli915Hang = new Version(5, 18); @@ -60,7 +70,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFixedKernel60i915Hang = new Version(6, 0, 18); private readonly Version _minKernelVersionAmdVkFmtModifier = new Version(5, 15); - private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0); + private readonly Version _minFFmpegImplicitHwaccel = new Version(6, 0); private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0); private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3); private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1); @@ -74,8 +84,9 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFFmpegQsvVppOutRangeOption = new Version(7, 0, 1); private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1); private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0); + private readonly Version _minFFmpegRkmppHevcDecDoviRpu = new Version(7, 1, 1); - private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled); + private static readonly Regex _containerValidationRegex = new(ContainerValidationRegex, RegexOptions.Compiled); private static readonly string[] _videoProfilesH264 = [ @@ -151,13 +162,22 @@ namespace MediaBrowser.Controller.MediaEncoding IMediaEncoder mediaEncoder, ISubtitleEncoder subtitleEncoder, IConfiguration config, - IConfigurationManager configurationManager) + IConfigurationManager configurationManager, + IPathManager pathManager) { _appPaths = appPaths; _mediaEncoder = mediaEncoder; _subtitleEncoder = subtitleEncoder; _config = config; _configurationManager = configurationManager; + _pathManager = pathManager; + } + + private enum DynamicHdrMetadataRemovalPlan + { + None, + RemoveDovi, + RemoveHdr10Plus, } [GeneratedRegex(@"\s+")] @@ -309,7 +329,6 @@ namespace MediaBrowser.Controller.MediaEncoding private bool IsSwTonemapAvailable(EncodingJobInfo state, EncodingOptions options) { if (state.VideoStream is null - || !options.EnableTonemapping || GetVideoColorBitDepth(state) < 10 || !_mediaEncoder.SupportsFilter("tonemapx")) { @@ -331,8 +350,17 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.VideoStream.VideoRange == VideoRange.HDR && state.VideoStream.VideoRangeType == VideoRangeType.DOVI) { - // Only native SW decoder and HW accelerator can parse dovi rpu. + // Only native SW decoder, HW accelerator and hevc_rkmpp decoder can parse dovi rpu. var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty; + + var isRkmppDecoder = vidDecoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase); + if (isRkmppDecoder + && _mediaEncoder.EncoderVersion >= _minFFmpegRkmppHevcDecDoviRpu + && string.Equals(state.VideoStream?.Codec, "hevc", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isNvdecDecoder = vidDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase); var isVaapiDecoder = vidDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); @@ -341,11 +369,8 @@ namespace MediaBrowser.Controller.MediaEncoding return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder || isVideoToolBoxDecoder; } - return state.VideoStream.VideoRange == VideoRange.HDR - && (state.VideoStream.VideoRangeType == VideoRangeType.HDR10 - || state.VideoStream.VideoRangeType == VideoRangeType.HLG - || state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10 - || state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG); + // GPU tonemapping supports all HDR RangeTypes + return state.VideoStream.VideoRange == VideoRange.HDR; } private bool IsVulkanHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options) @@ -380,8 +405,7 @@ namespace MediaBrowser.Controller.MediaEncoding } return state.VideoStream.VideoRange == VideoRange.HDR - && (state.VideoStream.VideoRangeType == VideoRangeType.HDR10 - || state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10); + && IsDoviWithHdr10Bl(state.VideoStream); } private bool IsVideoToolboxTonemapAvailable(EncodingJobInfo state, EncodingOptions options) @@ -396,7 +420,8 @@ namespace MediaBrowser.Controller.MediaEncoding // Certain DV profile 5 video works in Safari with direct playing, but the VideoToolBox does not produce correct mapping results with transcoding. // All other HDR formats working. return state.VideoStream.VideoRange == VideoRange.HDR - && state.VideoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG or VideoRangeType.HDR10Plus or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG; + && (IsDoviWithHdr10Bl(state.VideoStream) + || state.VideoStream.VideoRangeType is VideoRangeType.HLG); } private bool IsVideoStreamHevcRext(EncodingJobInfo state) @@ -451,7 +476,7 @@ namespace MediaBrowser.Controller.MediaEncoding return GetMjpegEncoder(state, encodingOptions); } - if (_validationRegex.IsMatch(codec)) + if (_containerValidationRegex.IsMatch(codec)) { return codec.ToLowerInvariant(); } @@ -492,7 +517,7 @@ namespace MediaBrowser.Controller.MediaEncoding public static string GetInputFormat(string container) { - if (string.IsNullOrEmpty(container) || !_validationRegex.IsMatch(container)) + if (string.IsNullOrEmpty(container) || !_containerValidationRegex.IsMatch(container)) { return null; } @@ -631,7 +656,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (string.IsNullOrWhiteSpace(container)) { - // this may not work, but if the client is that broken we can not do anything better + // this may not work, but if the client is that broken we cannot do anything better return "aac"; } @@ -710,7 +735,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var codec = state.OutputAudioCodec; - if (!_validationRegex.IsMatch(codec)) + if (!_containerValidationRegex.IsMatch(codec)) { codec = "aac"; } @@ -861,9 +886,9 @@ namespace MediaBrowser.Controller.MediaEncoding && _mediaEncoder.EncoderVersion >= _minFFmpegVaapiDeviceVendorId; // Priority: 'renderNodePath' > 'vendorId' > 'kernelDriver' - var driverOpts = string.IsNullOrEmpty(renderNodePath) - ? (haveVendorId ? $",vendor_id={vendorId}" : (string.IsNullOrEmpty(kernelDriver) ? string.Empty : $",kernel_driver={kernelDriver}")) - : renderNodePath; + var driverOpts = File.Exists(renderNodePath) + ? renderNodePath + : (haveVendorId ? $",vendor_id={vendorId}" : (string.IsNullOrEmpty(kernelDriver) ? string.Empty : $",kernel_driver={kernelDriver}")); // 'driver' behaves similarly to env LIBVA_DRIVER_NAME driverOpts += string.IsNullOrEmpty(driver) ? string.Empty : ",driver=" + driver; @@ -1300,6 +1325,13 @@ namespace MediaBrowser.Controller.MediaEncoding || codec.Contains("hevc", StringComparison.OrdinalIgnoreCase); } + public static bool IsAv1(MediaStream stream) + { + var codec = stream.Codec ?? string.Empty; + + return codec.Contains("av1", StringComparison.OrdinalIgnoreCase); + } + public static bool IsAAC(MediaStream stream) { var codec = stream.Codec ?? string.Empty; @@ -1307,8 +1339,125 @@ namespace MediaBrowser.Controller.MediaEncoding return codec.Contains("aac", StringComparison.OrdinalIgnoreCase); } - public static string GetBitStreamArgs(MediaStream stream) + public static bool IsDoviWithHdr10Bl(MediaStream stream) { + var rangeType = stream?.VideoRangeType; + + return rangeType is VideoRangeType.DOVIWithHDR10 + or VideoRangeType.DOVIWithEL + or VideoRangeType.DOVIWithHDR10Plus + or VideoRangeType.DOVIWithELHDR10Plus + or VideoRangeType.DOVIInvalid; + } + + public static bool IsDovi(MediaStream stream) + { + var rangeType = stream?.VideoRangeType; + + return IsDoviWithHdr10Bl(stream) + || (rangeType is VideoRangeType.DOVI + or VideoRangeType.DOVIWithHLG + or VideoRangeType.DOVIWithSDR); + } + + public static bool IsHdr10Plus(MediaStream stream) + { + var rangeType = stream?.VideoRangeType; + + return rangeType is VideoRangeType.HDR10Plus + or VideoRangeType.DOVIWithHDR10Plus + or VideoRangeType.DOVIWithELHDR10Plus; + } + + /// <summary> + /// Check if dynamic HDR metadata should be removed during stream copy. + /// Please note this check assumes the range check has already been done + /// and trivial fallbacks like HDR10+ to HDR10, DOVIWithHDR10 to HDR10 is already checked. + /// </summary> + private static DynamicHdrMetadataRemovalPlan ShouldRemoveDynamicHdrMetadata(EncodingJobInfo state) + { + var videoStream = state.VideoStream; + if (videoStream.VideoRange is not VideoRange.HDR) + { + return DynamicHdrMetadataRemovalPlan.None; + } + + var requestedRangeTypes = state.GetRequestedRangeTypes(state.VideoStream.Codec); + if (requestedRangeTypes.Length == 0) + { + return DynamicHdrMetadataRemovalPlan.None; + } + + var requestHasHDR10 = requestedRangeTypes.Contains(VideoRangeType.HDR10.ToString(), StringComparison.OrdinalIgnoreCase); + var requestHasDOVI = requestedRangeTypes.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase); + var requestHasDOVIwithEL = requestedRangeTypes.Contains(VideoRangeType.DOVIWithEL.ToString(), StringComparison.OrdinalIgnoreCase); + var requestHasDOVIwithELHDR10plus = requestedRangeTypes.Contains(VideoRangeType.DOVIWithELHDR10Plus.ToString(), StringComparison.OrdinalIgnoreCase); + + var shouldRemoveHdr10Plus = false; + // Case 1: Client supports HDR10, does not support DOVI with EL but EL presets + var shouldRemoveDovi = (!requestHasDOVIwithEL && requestHasHDR10) && videoStream.VideoRangeType == VideoRangeType.DOVIWithEL; + + // Case 2: Client supports DOVI, does not support broken DOVI config + // Client does not report DOVI support should be allowed to copy bad data for remuxing as HDR10 players would not crash + shouldRemoveDovi = shouldRemoveDovi || (requestHasDOVI && videoStream.VideoRangeType == VideoRangeType.DOVIInvalid); + + // Special case: we have a video with both EL and HDR10+ + // If the client supports EL but not in the case of coexistence with HDR10+, remove HDR10+ for compatibility reasons. + // Otherwise, remove DOVI if the client is not a DOVI player + if (videoStream.VideoRangeType == VideoRangeType.DOVIWithELHDR10Plus) + { + shouldRemoveHdr10Plus = requestHasDOVIwithEL && !requestHasDOVIwithELHDR10plus; + shouldRemoveDovi = shouldRemoveDovi || !shouldRemoveHdr10Plus; + } + + if (shouldRemoveDovi) + { + return DynamicHdrMetadataRemovalPlan.RemoveDovi; + } + + // If the client is a Dolby Vision Player, remove the HDR10+ metadata to avoid playback issues + shouldRemoveHdr10Plus = shouldRemoveHdr10Plus || (requestHasDOVI && videoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10Plus); + return shouldRemoveHdr10Plus ? DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus : DynamicHdrMetadataRemovalPlan.None; + } + + private bool CanEncoderRemoveDynamicHdrMetadata(DynamicHdrMetadataRemovalPlan plan, MediaStream videoStream) + { + return plan switch + { + DynamicHdrMetadataRemovalPlan.RemoveDovi => _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.DoviRpuStrip) + || (IsH265(videoStream) && _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.HevcMetadataRemoveDovi)) + || (IsAv1(videoStream) && _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.Av1MetadataRemoveDovi)), + DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus => (IsH265(videoStream) && _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.HevcMetadataRemoveHdr10Plus)) + || (IsAv1(videoStream) && _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.Av1MetadataRemoveHdr10Plus)), + _ => true, + }; + } + + public bool IsDoviRemoved(EncodingJobInfo state) + { + return state?.VideoStream is not null && ShouldRemoveDynamicHdrMetadata(state) == DynamicHdrMetadataRemovalPlan.RemoveDovi + && CanEncoderRemoveDynamicHdrMetadata(DynamicHdrMetadataRemovalPlan.RemoveDovi, state.VideoStream); + } + + public bool IsHdr10PlusRemoved(EncodingJobInfo state) + { + return state?.VideoStream is not null && ShouldRemoveDynamicHdrMetadata(state) == DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus + && CanEncoderRemoveDynamicHdrMetadata(DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus, state.VideoStream); + } + + public string GetBitStreamArgs(EncodingJobInfo state, MediaStreamType streamType) + { + if (state is null) + { + return null; + } + + var stream = streamType switch + { + MediaStreamType.Audio => state.AudioStream, + MediaStreamType.Video => state.VideoStream, + _ => state.VideoStream + }; // TODO This is auto inserted into the mpegts mux so it might not be needed. // https://www.ffmpeg.org/ffmpeg-bitstream-filters.html#h264_005fmp4toannexb if (IsH264(stream)) @@ -1316,21 +1465,57 @@ namespace MediaBrowser.Controller.MediaEncoding return "-bsf:v h264_mp4toannexb"; } + if (IsAAC(stream)) + { + // Convert adts header(mpegts) to asc header(mp4). + return "-bsf:a aac_adtstoasc"; + } + if (IsH265(stream)) { - return "-bsf:v hevc_mp4toannexb"; + var filter = "-bsf:v hevc_mp4toannexb"; + + // The following checks are not complete because the copy would be rejected + // if the encoder cannot remove required metadata. + // And if bsf is used, we must already be using copy codec. + switch (ShouldRemoveDynamicHdrMetadata(state)) + { + default: + case DynamicHdrMetadataRemovalPlan.None: + break; + case DynamicHdrMetadataRemovalPlan.RemoveDovi: + filter += _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.HevcMetadataRemoveDovi) + ? ",hevc_metadata=remove_dovi=1" + : ",dovi_rpu=strip=1"; + break; + case DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus: + filter += ",hevc_metadata=remove_hdr10plus=1"; + break; + } + + return filter; } - if (IsAAC(stream)) + if (IsAv1(stream)) { - // Convert adts header(mpegts) to asc header(mp4). - return "-bsf:a aac_adtstoasc"; + switch (ShouldRemoveDynamicHdrMetadata(state)) + { + default: + case DynamicHdrMetadataRemovalPlan.None: + return null; + case DynamicHdrMetadataRemovalPlan.RemoveDovi: + return _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.Av1MetadataRemoveDovi) + ? "-bsf:v av1_metadata=remove_dovi=1" + : "-bsf:v dovi_rpu=strip=1"; + case DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus: + return "-bsf:v av1_metadata=remove_hdr10plus=1"; + } } return null; } - public static string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer) + public string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer) { var bitStreamArgs = string.Empty; var segmentFormat = GetSegmentFileExtension(segmentContainer).TrimStart('.'); @@ -1341,7 +1526,7 @@ namespace MediaBrowser.Controller.MediaEncoding || string.Equals(mediaSourceContainer, "aac", StringComparison.OrdinalIgnoreCase) || string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase))) { - bitStreamArgs = GetBitStreamArgs(state.AudioStream); + bitStreamArgs = GetBitStreamArgs(state, MediaStreamType.Audio); bitStreamArgs = string.IsNullOrEmpty(bitStreamArgs) ? string.Empty : " " + bitStreamArgs; } @@ -1387,6 +1572,26 @@ namespace MediaBrowser.Controller.MediaEncoding return FormattableString.Invariant($" -maxrate {bitrate} -bufsize {bufsize}"); } + if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoCodec, "av1_qsv", StringComparison.OrdinalIgnoreCase)) + { + // TODO: probe QSV encoders' capabilities and enable more tuning options + // See also https://github.com/intel/media-delivery/blob/master/doc/quality.rst + + // Enable MacroBlock level bitrate control for better subjective visual quality + var mbbrcOpt = string.Empty; + if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) + { + mbbrcOpt = " -mbbrc 1"; + } + + // Set (maxrate == bitrate + 1) to trigger VBR for better bitrate allocation + // Set (rc_init_occupancy == 2 * bitrate) and (bufsize == 4 * bitrate) to deal with drastic scene changes + return FormattableString.Invariant($"{mbbrcOpt} -b:v {bitrate} -maxrate {bitrate + 1} -rc_init_occupancy {bitrate * 2} -bufsize {bitrate * 4}"); + } + if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "av1_amf", StringComparison.OrdinalIgnoreCase)) @@ -1620,7 +1825,7 @@ namespace MediaBrowser.Controller.MediaEncoding var alphaParam = enableAlpha ? ":alpha=1" : string.Empty; var sub2videoParam = enableSub2video ? ":sub2video=1" : string.Empty; - var fontPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id); + var fontPath = _pathManager.GetAttachmentFolderPath(state.MediaSource.Id); var fontParam = string.Format( CultureInfo.InvariantCulture, ":fontsdir='{0}'", @@ -2061,7 +2266,13 @@ namespace MediaBrowser.Controller.MediaEncoding // libx265 only accept level option in -x265-params. // level option may cause libx265 to fail. // libx265 cannot adjust the given level, just throw an error. - param += " -x265-params:0 subme=3:merange=25:rc-lookahead=10:me=star:ctu=32:max-tu-size=32:min-cu-size=16:rskip=2:rskip-edge-threshold=2:no-sao=1:no-strong-intra-smoothing=1:no-scenecut=1:no-open-gop=1:no-info=1"; + param += " -x265-params:0 no-scenecut=1:no-open-gop=1:no-info=1"; + + if (encodingOptions.EncoderPreset < EncoderPreset.ultrafast) + { + // The following params are slower than the ultrafast preset, don't use when ultrafast is selected. + param += ":subme=3:merange=25:rc-lookahead=10:me=star:ctu=32:max-tu-size=32:min-cu-size=16:rskip=2:rskip-edge-threshold=2:no-sao=1:no-strong-intra-smoothing=1"; + } } if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase) @@ -2162,7 +2373,6 @@ namespace MediaBrowser.Controller.MediaEncoding } // DOVIWithHDR10 should be compatible with HDR10 supporting players. Same goes with HLG and of course SDR. So allow copy of those formats - var requestHasHDR10 = requestedRangeTypes.Contains(VideoRangeType.HDR10.ToString(), StringComparison.OrdinalIgnoreCase); var requestHasHLG = requestedRangeTypes.Contains(VideoRangeType.HLG.ToString(), StringComparison.OrdinalIgnoreCase); var requestHasSDR = requestedRangeTypes.Contains(VideoRangeType.SDR.ToString(), StringComparison.OrdinalIgnoreCase); @@ -2170,9 +2380,17 @@ namespace MediaBrowser.Controller.MediaEncoding if (!requestedRangeTypes.Contains(videoStream.VideoRangeType.ToString(), StringComparison.OrdinalIgnoreCase) && !((requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10) || (requestHasHLG && videoStream.VideoRangeType == VideoRangeType.DOVIWithHLG) - || (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR))) - { - return false; + || (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR) + || (requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.HDR10Plus))) + { + // Check complicated cases where we need to remove dynamic metadata + // Conservatively refuse to copy if the encoder can't remove dynamic metadata, + // but a removal is required for compatability reasons. + var dynamicHdrMetadataRemovalPlan = ShouldRemoveDynamicHdrMetadata(state); + if (!CanEncoderRemoveDynamicHdrMetadata(dynamicHdrMetadataRemovalPlan, videoStream)) + { + return false; + } } } @@ -2197,7 +2415,7 @@ namespace MediaBrowser.Controller.MediaEncoding var videoFrameRate = videoStream.ReferenceFrameRate; // Add a little tolerance to the framerate check because some videos might record a framerate - // that is slightly higher than the intended framerate, but the device can still play it correctly. + // that is slightly greater than the intended framerate, but the device can still play it correctly. // 0.05 fps tolerance should be safe enough. if (!videoFrameRate.HasValue || videoFrameRate.Value > requestedFramerate.Value + 0.05f) { @@ -2684,10 +2902,10 @@ namespace MediaBrowser.Controller.MediaEncoding var seekTick = isHlsRemuxing ? time + 5000000L : time; // Seeking beyond EOF makes no sense in transcoding. Clamp the seekTick value to - // [0, RuntimeTicks - 0.5s], so that the muxer gets packets and avoid error codes. + // [0, RuntimeTicks - 5.0s], so that the muxer gets packets and avoid error codes. if (maxTime > 0) { - seekTick = Math.Clamp(seekTick, 0, Math.Max(maxTime - 5000000L, 0)); + seekTick = Math.Clamp(seekTick, 0, Math.Max(maxTime - 50000000L, 0)); } seekParam += string.Format(CultureInfo.InvariantCulture, "-ss {0}", _mediaEncoder.GetTimeParameter(seekTick)); @@ -3273,6 +3491,21 @@ namespace MediaBrowser.Controller.MediaEncoding doubleRateDeint ? "1" : "0"); } + if (hwDeintSuffix.Contains("opencl", StringComparison.OrdinalIgnoreCase)) + { + var useBwdif = options.DeinterlaceMethod == DeinterlaceMethod.bwdif; + + if (_mediaEncoder.SupportsFilter("yadif_opencl") + && _mediaEncoder.SupportsFilter("bwdif_opencl")) + { + return string.Format( + CultureInfo.InvariantCulture, + "{0}_opencl={1}:-1:0", + useBwdif ? "bwdif" : "yadif", + doubleRateDeint ? "1" : "0"); + } + } + if (hwDeintSuffix.Contains("vaapi", StringComparison.OrdinalIgnoreCase)) { return string.Format( @@ -3608,7 +3841,7 @@ namespace MediaBrowser.Controller.MediaEncoding return GetSwVidFilterChain(state, options, vidEncoder); } - // prefered nvdec/cuvid + cuda filters + nvenc pipeline + // preferred nvdec/cuvid + cuda filters + nvenc pipeline return GetNvidiaVidFiltersPrefered(state, options, vidDecoder, vidEncoder); } @@ -3649,8 +3882,8 @@ namespace MediaBrowser.Controller.MediaEncoding var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doCuTranspose = !string.IsNullOrEmpty(tranposeDir) && _mediaEncoder.SupportsFilter("transpose_cuda"); + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doCuTranspose = !string.IsNullOrEmpty(transposeDir) && _mediaEncoder.SupportsFilter("transpose_cuda"); var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isNvDecoder && doCuTranspose)); var swpInW = swapWAndH ? inH : inW; var swpInH = swapWAndH ? inW : inH; @@ -3696,7 +3929,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hw transpose if (doCuTranspose) { - mainFilters.Add($"transpose_cuda=dir={tranposeDir}"); + mainFilters.Add($"transpose_cuda=dir={transposeDir}"); } var isRext = IsVideoStreamHevcRext(state); @@ -3749,6 +3982,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasSubs) { + var alphaFormatOpt = string.Empty; if (hasGraphicalSubs) { var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); @@ -3766,10 +4000,13 @@ namespace MediaBrowser.Controller.MediaEncoding subFilters.Add(alphaSrcFilter); subFilters.Add("format=yuva420p"); subFilters.Add(subTextSubtitlesFilter); + + alphaFormatOpt = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayCudaAlphaFormat) + ? ":alpha_format=premultiplied" : string.Empty; } subFilters.Add("hwupload=derive_device=cuda"); - overlayFilters.Add("overlay_cuda=eof_action=pass:repeatlast=0"); + overlayFilters.Add($"overlay_cuda=eof_action=pass:repeatlast=0{alphaFormatOpt}"); } } else @@ -3816,7 +4053,7 @@ namespace MediaBrowser.Controller.MediaEncoding return GetSwVidFilterChain(state, options, vidEncoder); } - // prefered d3d11va + opencl filters + amf pipeline + // preferred d3d11va + opencl filters + amf pipeline return GetAmdDx11VidFiltersPrefered(state, options, vidDecoder, vidEncoder); } @@ -3856,8 +4093,8 @@ namespace MediaBrowser.Controller.MediaEncoding var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doOclTranspose = !string.IsNullOrEmpty(tranposeDir) + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doOclTranspose = !string.IsNullOrEmpty(transposeDir) && _mediaEncoder.SupportsFilterWithOption(FilterOptionType.TransposeOpenclReversal); var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isD3d11vaDecoder && doOclTranspose)); var swpInW = swapWAndH ? inH : inW; @@ -3901,12 +4138,17 @@ namespace MediaBrowser.Controller.MediaEncoding // map from d3d11va to opencl via d3d11-opencl interop. mainFilters.Add("hwmap=derive_device=opencl:mode=read"); - // hw deint <= TODO: finsh the 'yadif_opencl' filter + // hw deint + if (doDeintH2645) + { + var deintFilter = GetHwDeinterlaceFilter(state, options, "opencl"); + mainFilters.Add(deintFilter); + } // hw transpose if (doOclTranspose) { - mainFilters.Add($"transpose_opencl=dir={tranposeDir}"); + mainFilters.Add($"transpose_opencl=dir={transposeDir}"); } var outFormat = doOclTonemap ? string.Empty : "nv12"; @@ -3966,6 +4208,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasSubs) { + var alphaFormatOpt = string.Empty; if (hasGraphicalSubs) { var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); @@ -3983,10 +4226,13 @@ namespace MediaBrowser.Controller.MediaEncoding subFilters.Add(alphaSrcFilter); subFilters.Add("format=yuva420p"); subFilters.Add(subTextSubtitlesFilter); + + alphaFormatOpt = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayOpenclAlphaFormat) + ? ":alpha_format=premultiplied" : string.Empty; } subFilters.Add("hwupload=derive_device=opencl"); - overlayFilters.Add("overlay_opencl=eof_action=pass:repeatlast=0"); + overlayFilters.Add($"overlay_opencl=eof_action=pass:repeatlast=0{alphaFormatOpt}"); overlayFilters.Add("hwmap=derive_device=d3d11va:mode=write:reverse=1"); overlayFilters.Add("format=d3d11"); } @@ -4042,13 +4288,13 @@ namespace MediaBrowser.Controller.MediaEncoding return GetSwVidFilterChain(state, options, vidEncoder); } - // prefered qsv(vaapi) + opencl filters pipeline + // preferred qsv(vaapi) + opencl filters pipeline if (isIntelVaapiOclSupported) { return GetIntelQsvVaapiVidFiltersPrefered(state, options, vidDecoder, vidEncoder); } - // prefered qsv(d3d11) + opencl filters pipeline + // preferred qsv(d3d11) + opencl filters pipeline if (isIntelDx11OclSupported) { return GetIntelQsvDx11VidFiltersPrefered(state, options, vidDecoder, vidEncoder); @@ -4097,8 +4343,8 @@ namespace MediaBrowser.Controller.MediaEncoding var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doVppTranspose = !string.IsNullOrEmpty(tranposeDir); + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doVppTranspose = !string.IsNullOrEmpty(transposeDir); var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || ((isD3d11vaDecoder || isQsvDecoder) && doVppTranspose)); var swpInW = swapWAndH ? inH : inW; var swpInH = swapWAndH ? inW : inH; @@ -4191,7 +4437,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(hwScaleFilter) && doVppTranspose) { - hwScaleFilter += $":transpose={tranposeDir}"; + hwScaleFilter += $":transpose={transposeDir}"; } if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder) @@ -4384,8 +4630,8 @@ namespace MediaBrowser.Controller.MediaEncoding var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doVppTranspose = !string.IsNullOrEmpty(tranposeDir); + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doVppTranspose = !string.IsNullOrEmpty(transposeDir); var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || ((isVaapiDecoder || isQsvDecoder) && doVppTranspose)); var swpInW = swapWAndH ? inH : inW; var swpInH = swapWAndH ? inW : inH; @@ -4445,7 +4691,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hw transpose(vaapi vpp) if (isVaapiDecoder && doVppTranspose) { - mainFilters.Add($"transpose_vaapi=dir={tranposeDir}"); + mainFilters.Add($"transpose_vaapi=dir={transposeDir}"); } var outFormat = doTonemap ? (((isQsvDecoder && doVppTranspose) || isRext) ? "p010" : string.Empty) : "nv12"; @@ -4455,7 +4701,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(hwScaleFilter) && isQsvDecoder && doVppTranspose) { - hwScaleFilter += $":transpose={tranposeDir}"; + hwScaleFilter += $":transpose={transposeDir}"; } if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder) @@ -4656,14 +4902,14 @@ namespace MediaBrowser.Controller.MediaEncoding return swFilterChain; } - // prefered vaapi + opencl filters pipeline + // preferred vaapi + opencl filters pipeline if (_mediaEncoder.IsVaapiDeviceInteliHD) { // Intel iHD path, with extra vpp tonemap and overlay support. return GetIntelVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder); } - // prefered vaapi + vulkan filters pipeline + // preferred vaapi + vulkan filters pipeline if (_mediaEncoder.IsVaapiDeviceAmd && isVaapiVkSupported && _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop @@ -4715,8 +4961,8 @@ namespace MediaBrowser.Controller.MediaEncoding var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doVaVppTranspose = !string.IsNullOrEmpty(tranposeDir); + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doVaVppTranspose = !string.IsNullOrEmpty(transposeDir); var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isVaapiDecoder && doVaVppTranspose)); var swpInW = swapWAndH ? inH : inW; var swpInH = swapWAndH ? inW : inH; @@ -4771,7 +5017,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hw transpose if (doVaVppTranspose) { - mainFilters.Add($"transpose_vaapi=dir={tranposeDir}"); + mainFilters.Add($"transpose_vaapi=dir={transposeDir}"); } var outFormat = doTonemap ? (isRext ? "p010" : string.Empty) : "nv12"; @@ -4948,8 +5194,8 @@ namespace MediaBrowser.Controller.MediaEncoding || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doVkTranspose = isVaapiDecoder && !string.IsNullOrEmpty(tranposeDir); + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doVkTranspose = isVaapiDecoder && !string.IsNullOrEmpty(transposeDir); var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isVaapiDecoder && doVkTranspose)); var swpInW = swapWAndH ? inH : inW; var swpInH = swapWAndH ? inW : inH; @@ -5042,13 +5288,13 @@ namespace MediaBrowser.Controller.MediaEncoding // vk transpose if (doVkTranspose) { - if (string.Equals(tranposeDir, "reversal", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(transposeDir, "reversal", StringComparison.OrdinalIgnoreCase)) { mainFilters.Add("flip_vulkan"); } else { - mainFilters.Add($"transpose_vulkan=dir={tranposeDir}"); + mainFilters.Add($"transpose_vulkan=dir={transposeDir}"); } } @@ -5416,8 +5662,8 @@ namespace MediaBrowser.Controller.MediaEncoding var usingHwSurface = isVtDecoder && (_mediaEncoder.EncoderVersion >= _minFFmpegWorkingVtHwSurface); var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doVtTranspose = !string.IsNullOrEmpty(tranposeDir) && _mediaEncoder.SupportsFilter("transpose_vt"); + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doVtTranspose = !string.IsNullOrEmpty(transposeDir) && _mediaEncoder.SupportsFilter("transpose_vt"); var swapWAndH = Math.Abs(rotation) == 90 && doVtTranspose; var swpInW = swapWAndH ? inH : inW; var swpInH = swapWAndH ? inW : inH; @@ -5461,7 +5707,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hw transpose if (doVtTranspose) { - mainFilters.Add($"transpose_vt=dir={tranposeDir}"); + mainFilters.Add($"transpose_vt=dir={transposeDir}"); } if (doVtTonemap) @@ -5576,7 +5822,7 @@ namespace MediaBrowser.Controller.MediaEncoding return GetSwVidFilterChain(state, options, vidEncoder); } - // prefered rkmpp + rkrga + opencl filters pipeline + // preferred rkmpp + rkrga + opencl filters pipeline if (isRkmppOclSupported) { return GetRkmppVidFiltersPrefered(state, options, vidDecoder, vidEncoder); @@ -5614,7 +5860,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doDeintH2645 = doDeintH264 || doDeintHevc; var doOclTonemap = IsHwTonemapAvailable(state, options); - var hasSubs = state.SubtitleStream != null && ShouldEncodeSubtitle(state); + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -5624,8 +5870,8 @@ namespace MediaBrowser.Controller.MediaEncoding var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doRkVppTranspose = !string.IsNullOrEmpty(tranposeDir); + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doRkVppTranspose = !string.IsNullOrEmpty(transposeDir); var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isRkmppDecoder && doRkVppTranspose)); var swpInW = swapWAndH ? inH : inW; var swpInH = swapWAndH ? inW : inH; @@ -5690,13 +5936,17 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(doScaling) && !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f)) { - var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={outFormat}:afbc=1"; + // Vendor provided BSP kernel has an RGA driver bug that causes the output to be corrupted for P010 format. + // Use NV15 instead of P010 to avoid the issue. + // SDR inputs are using BGRA formats already which is not affected. + var intermediateFormat = string.Equals(outFormat, "p010", StringComparison.OrdinalIgnoreCase) ? "nv15" : outFormat; + var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={intermediateFormat}:force_original_aspect_ratio=increase:force_divisible_by=4:afbc=1"; mainFilters.Add(hwScaleFilterFirstPass); } if (!string.IsNullOrEmpty(hwScaleFilter) && doRkVppTranspose) { - hwScaleFilter += $":transpose={tranposeDir}"; + hwScaleFilter += $":transpose={transposeDir}"; } // try enabling AFBC to save DDR bandwidth @@ -5768,9 +6018,10 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasSubs) { + var subMaxH = 1080; if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, subMaxH); subFilters.Add(subPreProcFilters); subFilters.Add("format=bgra"); } @@ -5780,7 +6031,7 @@ namespace MediaBrowser.Controller.MediaEncoding var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10; // alphasrc=s=1280x720:r=10:start=0,format=bgra,subtitles,hwupload - var alphaSrcFilter = GetAlphaSrcFilter(state, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH, subFramerate); + var alphaSrcFilter = GetAlphaSrcFilter(state, swpInW, swpInH, reqW, reqH, reqMaxW, subMaxH, subFramerate); var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true); subFilters.Add(alphaSrcFilter); subFilters.Add("format=bgra"); @@ -5789,6 +6040,13 @@ namespace MediaBrowser.Controller.MediaEncoding subFilters.Add("hwupload=derive_device=rkmpp"); + // offload 1080p+ subtitles swscale upscaling from CPU to RGA + var (overlayW, overlayH) = GetFixedOutputSize(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + if (overlayW.HasValue && overlayH.HasValue && overlayH.Value > subMaxH) + { + subFilters.Add($"vpp_rkrga=w={overlayW.Value}:h={overlayH.Value}:format=bgra:afbc=1"); + } + // try enabling AFBC to save DDR bandwidth var hwOverlayFilter = "overlay_rkrga=eof_action=pass:repeatlast=0:format=nv12"; if (isEncoderSupportAfbc) @@ -6170,7 +6428,7 @@ namespace MediaBrowser.Controller.MediaEncoding var ffmpegVersion = _mediaEncoder.EncoderVersion; // Set the av1 codec explicitly to trigger hw accelerator, otherwise libdav1d will be used. - var isAv1 = ffmpegVersion < _minFFmpegImplictHwaccel + var isAv1 = ffmpegVersion < _minFFmpegImplicitHwaccel && string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase); // Allow profile mismatch if decoding H.264 baseline with d3d11va and vaapi hwaccels. @@ -6610,6 +6868,7 @@ namespace MediaBrowser.Controller.MediaEncoding || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) || string.Equals("yuv422p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) || string.Equals("yuv444p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); + var isAv1SupportedSwFormatsVt = is8_10bitSwFormatsVt || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); // The related patches make videotoolbox hardware surface working is only available in jellyfin-ffmpeg 7.0.1 at the moment. bool useHwSurface = (_mediaEncoder.EncoderVersion >= _minFFmpegWorkingVtHwSurface) && IsVideoToolboxFullSupported(); @@ -6643,6 +6902,13 @@ namespace MediaBrowser.Controller.MediaEncoding { return GetHwaccelType(state, options, "hevc", bitDepth, useHwSurface); } + + if (string.Equals("av1", videoStream.Codec, StringComparison.OrdinalIgnoreCase) + && isAv1SupportedSwFormatsVt + && _mediaEncoder.IsVideoToolboxAv1DecodeAvailable) + { + return GetHwaccelType(state, options, "av1", bitDepth, useHwSurface); + } } return null; @@ -6737,7 +7003,8 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(videoStream.Codec, "av1", StringComparison.OrdinalIgnoreCase)) { - return GetHwaccelType(state, options, "av1", bitDepth, hwSurface); + var accelType = GetHwaccelType(state, options, "av1", bitDepth, hwSurface); + return accelType + ((!string.IsNullOrEmpty(accelType) && isAfbcSupported) ? " -afbc rga" : string.Empty); } } @@ -6971,7 +7238,7 @@ namespace MediaBrowser.Controller.MediaEncoding state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders; state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate; - if (state.ReadInputAtNativeFramerate + if ((state.ReadInputAtNativeFramerate && !state.IsSegmentedLiveStream) || (mediaSource.Protocol == MediaProtocol.File && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase))) { @@ -7064,7 +7331,7 @@ namespace MediaBrowser.Controller.MediaEncoding { // DTS and TrueHD are not supported by HLS // Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used - shiftAudioCodecs.Add("dca"); + shiftAudioCodecs.Add("dts"); shiftAudioCodecs.Add("truehd"); } else @@ -7225,7 +7492,7 @@ namespace MediaBrowser.Controller.MediaEncoding && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) { - string bitStreamArgs = GetBitStreamArgs(state.VideoStream); + string bitStreamArgs = GetBitStreamArgs(state, MediaStreamType.Video); if (!string.IsNullOrEmpty(bitStreamArgs)) { args += " " + bitStreamArgs; diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index caa312987..8d6211051 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -4,10 +4,11 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using System.Linq; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; diff --git a/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs index a2b6e1d73..6ad953023 100644 --- a/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs +++ b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs @@ -38,6 +38,16 @@ namespace MediaBrowser.Controller.MediaEncoding /// <summary> /// The transpose_opencl_reversal. /// </summary> - TransposeOpenclReversal = 6 + TransposeOpenclReversal = 6, + + /// <summary> + /// The overlay_opencl_alpha_format. + /// </summary> + OverlayOpenclAlphaFormat = 7, + + /// <summary> + /// The overlay_cuda_alpha_format. + /// </summary> + OverlayCudaAlphaFormat = 8 } } diff --git a/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs index 09840d2ee..d8d136472 100644 --- a/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs +++ b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs @@ -9,26 +9,33 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -namespace MediaBrowser.Controller.MediaEncoding -{ - public interface IAttachmentExtractor - { - Task<(MediaAttachment Attachment, Stream Stream)> GetAttachment( - BaseItem item, - string mediaSourceId, - int attachmentStreamIndex, - CancellationToken cancellationToken); +namespace MediaBrowser.Controller.MediaEncoding; - Task ExtractAllAttachments( - string inputFile, - MediaSourceInfo mediaSource, - string outputPath, - CancellationToken cancellationToken); +public interface IAttachmentExtractor +{ + /// <summary> + /// Gets the path to the attachment file. + /// </summary> + /// <param name="item">The <see cref="BaseItem"/>.</param> + /// <param name="mediaSourceId">The media source id.</param> + /// <param name="attachmentStreamIndex">The attachment index.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The async task.</returns> + Task<(MediaAttachment Attachment, Stream Stream)> GetAttachment( + BaseItem item, + string mediaSourceId, + int attachmentStreamIndex, + CancellationToken cancellationToken); - Task ExtractAllAttachmentsExternal( - string inputArgument, - string id, - string outputPath, - CancellationToken cancellationToken); - } + /// <summary> + /// Gets the path to the attachment file. + /// </summary> + /// <param name="inputFile">The input file path.</param> + /// <param name="mediaSource">The <see cref="MediaSourceInfo" /> source id.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The async task.</returns> + Task ExtractAllAttachments( + string inputFile, + MediaSourceInfo mediaSource, + CancellationToken cancellationToken); } diff --git a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs deleted file mode 100644 index 8ce40a58d..000000000 --- a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs +++ /dev/null @@ -1,28 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Controller.MediaEncoding -{ - public interface IEncodingManager - { - /// <summary> - /// Refreshes the chapter images. - /// </summary> - /// <param name="video">Video to use.</param> - /// <param name="directoryService">Directory service to use.</param> - /// <param name="chapters">Set of chapters to refresh.</param> - /// <param name="extractImages">Option to extract images.</param> - /// <param name="saveChapters">Option to save chapters.</param> - /// <param name="cancellationToken">CancellationToken to use for operation.</param> - /// <returns><c>true</c> if successful, <c>false</c> if not.</returns> - Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken); - } -} diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index c767b4a51..de6353c4c 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -76,6 +76,12 @@ namespace MediaBrowser.Controller.MediaEncoding bool IsVaapiDeviceSupportVulkanDrmInterop { get; } /// <summary> + /// Gets a value indicating whether av1 decoding is available via VideoToolbox. + /// </summary> + /// <value><c>true</c> if the av1 is available via VideoToolbox, <c>false</c> otherwise.</value> + bool IsVideoToolboxAv1DecodeAvailable { get; } + + /// <summary> /// Whether given encoder codec is supported. /// </summary> /// <param name="encoder">The encoder.</param> @@ -111,6 +117,13 @@ namespace MediaBrowser.Controller.MediaEncoding bool SupportsFilterWithOption(FilterOptionType option); /// <summary> + /// Whether the bitstream filter is supported with the given option. + /// </summary> + /// <param name="option">The option.</param> + /// <returns><c>true</c> if the bitstream filter is supported, <c>false</c> otherwise.</returns> + bool SupportsBitStreamFilterWithOption(BitStreamFilterOptionType option); + + /// <summary> /// Extracts the audio image. /// </summary> /// <param name="path">The path.</param> diff --git a/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs index 672f27eca..720c607f1 100644 --- a/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs +++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs @@ -2,12 +2,13 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.MediaSegments; -namespace MediaBrowser.Controller; +namespace MediaBrowser.Controller.MediaSegments; /// <summary> /// Defines methods for interacting with media segments. @@ -18,10 +19,11 @@ public interface IMediaSegmentManager /// Uses all segment providers enabled for the <see cref="BaseItem"/>'s library to get the Media Segments. /// </summary> /// <param name="baseItem">The Item to evaluate.</param> + /// <param name="libraryOptions">The library options.</param> /// <param name="overwrite">If set, will remove existing segments and replace it with new ones otherwise will check for existing segments and if found any, stops.</param> - /// <param name="cancellationToken">stop request token.</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task that indicates the Operation is finished.</returns> - Task RunSegmentPluginProviders(BaseItem baseItem, bool overwrite, CancellationToken cancellationToken); + Task RunSegmentPluginProviders(BaseItem baseItem, LibraryOptions libraryOptions, bool overwrite, CancellationToken cancellationToken); /// <summary> /// Returns if this item supports media segments. @@ -46,22 +48,22 @@ public interface IMediaSegmentManager Task DeleteSegmentAsync(Guid segmentId); /// <summary> - /// Obtains all segments accociated with the itemId. + /// Deletes all media segments of an item. /// </summary> - /// <param name="itemId">The id of the <see cref="BaseItem"/>.</param> - /// <param name="typeFilter">filteres all media segments of the given type to be included. If null all types are included.</param> - /// <param name="filterByProvider">When set filteres the segments to only return those that which providers are currently enabled on their library.</param> - /// <returns>An enumerator of <see cref="MediaSegmentDto"/>'s.</returns> - Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true); + /// <param name="itemId">The <see cref="BaseItem.Id"/> to delete all segments for.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>a task.</returns> + Task DeleteSegmentsAsync(Guid itemId, CancellationToken cancellationToken); /// <summary> - /// Obtains all segments accociated with the itemId. + /// Obtains all segments associated with the itemId. /// </summary> /// <param name="item">The <see cref="BaseItem"/>.</param> - /// <param name="typeFilter">filteres all media segments of the given type to be included. If null all types are included.</param> - /// <param name="filterByProvider">When set filteres the segments to only return those that which providers are currently enabled on their library.</param> + /// <param name="typeFilter">filters all media segments of the given type to be included. If null all types are included.</param> + /// <param name="libraryOptions">The library options.</param> + /// <param name="filterByProvider">When set filters the segments to only return those that which providers are currently enabled on their library.</param> /// <returns>An enumerator of <see cref="MediaSegmentDto"/>'s.</returns> - Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(BaseItem item, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true); + Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(BaseItem item, IEnumerable<MediaSegmentType>? typeFilter, LibraryOptions libraryOptions, bool filterByProvider = true); /// <summary> /// Gets information about any media segments stored for the given itemId. diff --git a/MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs index 39bb58bef..5a6d15d78 100644 --- a/MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs +++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; using MediaBrowser.Model; using MediaBrowser.Model.MediaSegments; -namespace MediaBrowser.Controller; +namespace MediaBrowser.Controller.MediaSegments; /// <summary> /// Provides methods for Obtaining the Media Segments from an Item. diff --git a/MediaBrowser.Controller/Net/AuthorizationInfo.cs b/MediaBrowser.Controller/Net/AuthorizationInfo.cs index 2452b25ab..dd5eb9a01 100644 --- a/MediaBrowser.Controller/Net/AuthorizationInfo.cs +++ b/MediaBrowser.Controller/Net/AuthorizationInfo.cs @@ -1,7 +1,6 @@ -#nullable disable - using System; -using Jellyfin.Data.Entities; +using System.Diagnostics.CodeAnalysis; +using Jellyfin.Database.Implementations.Entities; namespace MediaBrowser.Controller.Net { @@ -20,31 +19,31 @@ namespace MediaBrowser.Controller.Net /// Gets or sets the device identifier. /// </summary> /// <value>The device identifier.</value> - public string DeviceId { get; set; } + public string? DeviceId { get; set; } /// <summary> /// Gets or sets the device. /// </summary> /// <value>The device.</value> - public string Device { get; set; } + public string? Device { get; set; } /// <summary> /// Gets or sets the client. /// </summary> /// <value>The client.</value> - public string Client { get; set; } + public string? Client { get; set; } /// <summary> /// Gets or sets the version. /// </summary> /// <value>The version.</value> - public string Version { get; set; } + public string? Version { get; set; } /// <summary> /// Gets or sets the token. /// </summary> /// <value>The token.</value> - public string Token { get; set; } + public string? Token { get; set; } /// <summary> /// Gets or sets a value indicating whether the authorization is from an api key. @@ -54,7 +53,7 @@ namespace MediaBrowser.Controller.Net /// <summary> /// Gets or sets the user making the request. /// </summary> - public User User { get; set; } + public User? User { get; set; } /// <summary> /// Gets or sets a value indicating whether the token is authenticated. @@ -62,8 +61,9 @@ namespace MediaBrowser.Controller.Net public bool IsAuthenticated { get; set; } /// <summary> - /// Gets or sets a value indicating whether the request has a token. + /// Gets a value indicating whether the request has a token. /// </summary> - public bool HasToken { get; set; } + [MemberNotNullWhen(true, nameof(Token))] + public bool HasToken => !string.IsNullOrWhiteSpace(Token); } } diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs index bba5a6b85..bdc0f9a10 100644 --- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs +++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs @@ -24,9 +24,9 @@ namespace MediaBrowser.Controller.Net DateTime LastActivityDate { get; } /// <summary> - /// Gets or sets the date of last Keeplive received. + /// Gets or sets the date of last Keepalive received. /// </summary> - /// <value>The date of last Keeplive received.</value> + /// <value>The date of last Keepalive received.</value> DateTime LastKeepAliveDate { get; set; } /// <summary> diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs deleted file mode 100644 index 6a501aa7e..000000000 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.ComponentModel; -using MediaBrowser.Model.Session; -using MediaBrowser.Model.SyncPlay; - -namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; - -/// <summary> -/// Untyped sync play command. -/// </summary> -public class SyncPlayGroupUpdateCommandMessage : OutboundWebSocketMessage<GroupUpdate> -{ - /// <summary> - /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandMessage"/> class. - /// </summary> - /// <param name="data">The send command.</param> - public SyncPlayGroupUpdateCommandMessage(GroupUpdate data) - : base(data) - { - } - - /// <inheritdoc /> - [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] - public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; -} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs deleted file mode 100644 index 47f706e2a..000000000 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.ComponentModel; -using MediaBrowser.Model.Session; -using MediaBrowser.Model.SyncPlay; - -namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; - -/// <summary> -/// Sync play group update command with group info. -/// GroupUpdateTypes: GroupJoined. -/// </summary> -public class SyncPlayGroupUpdateCommandOfGroupInfoMessage : OutboundWebSocketMessage<GroupUpdate<GroupInfoDto>> -{ - /// <summary> - /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfGroupInfoMessage"/> class. - /// </summary> - /// <param name="data">The group info.</param> - public SyncPlayGroupUpdateCommandOfGroupInfoMessage(GroupUpdate<GroupInfoDto> data) - : base(data) - { - } - - /// <inheritdoc /> - [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] - public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; -} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs deleted file mode 100644 index 11ddb1e25..000000000 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.ComponentModel; -using MediaBrowser.Model.Session; -using MediaBrowser.Model.SyncPlay; - -namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; - -/// <summary> -/// Sync play group update command with group state update. -/// GroupUpdateTypes: StateUpdate. -/// </summary> -public class SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage : OutboundWebSocketMessage<GroupUpdate<GroupStateUpdate>> -{ - /// <summary> - /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage"/> class. - /// </summary> - /// <param name="data">The group info.</param> - public SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage(GroupUpdate<GroupStateUpdate> data) - : base(data) - { - } - - /// <inheritdoc /> - [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] - public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; -} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs deleted file mode 100644 index 7e73399b1..000000000 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.ComponentModel; -using MediaBrowser.Model.Session; -using MediaBrowser.Model.SyncPlay; - -namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; - -/// <summary> -/// Sync play group update command with play queue update. -/// GroupUpdateTypes: PlayQueue. -/// </summary> -public class SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage : OutboundWebSocketMessage<GroupUpdate<PlayQueueUpdate>> -{ - /// <summary> - /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage"/> class. - /// </summary> - /// <param name="data">The play queue update.</param> - public SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage(GroupUpdate<PlayQueueUpdate> data) - : base(data) - { - } - - /// <inheritdoc /> - [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] - public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; -} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs deleted file mode 100644 index 5b5ccd3ed..000000000 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.ComponentModel; -using MediaBrowser.Model.Session; -using MediaBrowser.Model.SyncPlay; - -namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; - -/// <summary> -/// Sync play group update command with string. -/// GroupUpdateTypes: GroupDoesNotExist (error), LibraryAccessDenied (error), NotInGroup (error), GroupLeft (groupId), UserJoined (username), UserLeft (username). -/// </summary> -public class SyncPlayGroupUpdateCommandOfStringMessage : OutboundWebSocketMessage<GroupUpdate<string>> -{ - /// <summary> - /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfStringMessage"/> class. - /// </summary> - /// <param name="data">The send command.</param> - public SyncPlayGroupUpdateCommandOfStringMessage(GroupUpdate<string> data) - : base(data) - { - } - - /// <inheritdoc /> - [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] - public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; -} diff --git a/MediaBrowser.Controller/Persistence/IChapterRepository.cs b/MediaBrowser.Controller/Persistence/IChapterRepository.cs new file mode 100644 index 000000000..0844ddb36 --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IChapterRepository.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Persistence; + +/// <summary> +/// Interface IChapterRepository. +/// </summary> +public interface IChapterRepository +{ + /// <summary> + /// Deletes the chapters. + /// </summary> + /// <param name="itemId">The item.</param> + void DeleteChapters(Guid itemId); + + /// <summary> + /// Saves the chapters. + /// </summary> + /// <param name="itemId">The item.</param> + /// <param name="chapters">The set of chapters.</param> + void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters); + + /// <summary> + /// Gets all chapters associated with the baseItem. + /// </summary> + /// <param name="baseItemId">The BaseItems id.</param> + /// <returns>A readonly list of chapter instances.</returns> + IReadOnlyList<ChapterInfo> GetChapters(Guid baseItemId); + + /// <summary> + /// Gets a single chapter of a BaseItem on a specific index. + /// </summary> + /// <param name="baseItemId">The BaseItems id.</param> + /// <param name="index">The index of that chapter.</param> + /// <returns>A chapter instance.</returns> + ChapterInfo? GetChapter(Guid baseItemId, int index); +} diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 2c52b2b45..e185898bf 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -5,159 +5,101 @@ using System; using System.Collections.Generic; using System.Threading; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; -namespace MediaBrowser.Controller.Persistence +namespace MediaBrowser.Controller.Persistence; + +/// <summary> +/// Provides an interface to implement an Item repository. +/// </summary> +public interface IItemRepository { /// <summary> - /// Provides an interface to implement an Item repository. + /// Deletes the item. + /// </summary> + /// <param name="id">The identifier.</param> + void DeleteItem(Guid id); + + /// <summary> + /// Saves the items. + /// </summary> + /// <param name="items">The items.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken); + + void SaveImages(BaseItem item); + + /// <summary> + /// Retrieves the item. /// </summary> - public interface IItemRepository : IDisposable - { - /// <summary> - /// Deletes the item. - /// </summary> - /// <param name="id">The identifier.</param> - void DeleteItem(Guid id); - - /// <summary> - /// Saves the items. - /// </summary> - /// <param name="items">The items.</param> - /// <param name="cancellationToken">The cancellation token.</param> - void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken); - - void SaveImages(BaseItem item); - - /// <summary> - /// Retrieves the item. - /// </summary> - /// <param name="id">The id.</param> - /// <returns>BaseItem.</returns> - BaseItem RetrieveItem(Guid id); - - /// <summary> - /// Gets chapters for an item. - /// </summary> - /// <param name="item">The item.</param> - /// <returns>The list of chapter info.</returns> - List<ChapterInfo> GetChapters(BaseItem item); - - /// <summary> - /// Gets a single chapter for an item. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="index">The chapter index.</param> - /// <returns>The chapter info at the specified index.</returns> - ChapterInfo GetChapter(BaseItem item, int index); - - /// <summary> - /// Saves the chapters. - /// </summary> - /// <param name="id">The item id.</param> - /// <param name="chapters">The list of chapters to save.</param> - void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters); - - /// <summary> - /// Gets the media streams. - /// </summary> - /// <param name="query">The query.</param> - /// <returns>IEnumerable{MediaStream}.</returns> - List<MediaStream> GetMediaStreams(MediaStreamQuery query); - - /// <summary> - /// Saves the media streams. - /// </summary> - /// <param name="id">The identifier.</param> - /// <param name="streams">The streams.</param> - /// <param name="cancellationToken">The cancellation token.</param> - void SaveMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, CancellationToken cancellationToken); - - /// <summary> - /// Gets the media attachments. - /// </summary> - /// <param name="query">The query.</param> - /// <returns>IEnumerable{MediaAttachment}.</returns> - List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query); - - /// <summary> - /// Saves the media attachments. - /// </summary> - /// <param name="id">The identifier.</param> - /// <param name="attachments">The attachments.</param> - /// <param name="cancellationToken">The cancellation token.</param> - void SaveMediaAttachments(Guid id, IReadOnlyList<MediaAttachment> attachments, CancellationToken cancellationToken); - - /// <summary> - /// Gets the items. - /// </summary> - /// <param name="query">The query.</param> - /// <returns>QueryResult<BaseItem>.</returns> - QueryResult<BaseItem> GetItems(InternalItemsQuery query); - - /// <summary> - /// Gets the item ids list. - /// </summary> - /// <param name="query">The query.</param> - /// <returns>List<Guid>.</returns> - List<Guid> GetItemIdsList(InternalItemsQuery query); - - /// <summary> - /// Gets the people. - /// </summary> - /// <param name="query">The query.</param> - /// <returns>List<PersonInfo>.</returns> - List<PersonInfo> GetPeople(InternalPeopleQuery query); - - /// <summary> - /// Updates the people. - /// </summary> - /// <param name="itemId">The item identifier.</param> - /// <param name="people">The people.</param> - void UpdatePeople(Guid itemId, List<PersonInfo> people); - - /// <summary> - /// Gets the people names. - /// </summary> - /// <param name="query">The query.</param> - /// <returns>List<System.String>.</returns> - List<string> GetPeopleNames(InternalPeopleQuery query); - - /// <summary> - /// Gets the item list. - /// </summary> - /// <param name="query">The query.</param> - /// <returns>List<BaseItem>.</returns> - List<BaseItem> GetItemList(InternalItemsQuery query); - - /// <summary> - /// Updates the inherited values. - /// </summary> - void UpdateInheritedValues(); - - int GetCount(InternalItemsQuery query); - - QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query); - - QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query); - - QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query); - - QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query); - - QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query); - - QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query); - - List<string> GetMusicGenreNames(); - - List<string> GetStudioNames(); - - List<string> GetGenreNames(); - - List<string> GetAllArtistNames(); - } + /// <param name="id">The id.</param> + /// <returns>BaseItem.</returns> + BaseItem RetrieveItem(Guid id); + + /// <summary> + /// Gets the items. + /// </summary> + /// <param name="filter">The query.</param> + /// <returns>QueryResult<BaseItem>.</returns> + QueryResult<BaseItem> GetItems(InternalItemsQuery filter); + + /// <summary> + /// Gets the item ids list. + /// </summary> + /// <param name="filter">The query.</param> + /// <returns>List<Guid>.</returns> + IReadOnlyList<Guid> GetItemIdsList(InternalItemsQuery filter); + + /// <summary> + /// Gets the item list. + /// </summary> + /// <param name="filter">The query.</param> + /// <returns>List<BaseItem>.</returns> + IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery filter); + + /// <summary> + /// Gets the item list. Used mainly by the Latest api endpoint. + /// </summary> + /// <param name="filter">The query.</param> + /// <param name="collectionType">Collection Type.</param> + /// <returns>List<BaseItem>.</returns> + IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery filter, CollectionType collectionType); + + /// <summary> + /// Gets the list of series presentation keys for next up. + /// </summary> + /// <param name="filter">The query.</param> + /// <param name="dateCutoff">The minimum date for a series to have been most recently watched.</param> + /// <returns>The list of keys.</returns> + IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff); + + /// <summary> + /// Updates the inherited values. + /// </summary> + void UpdateInheritedValues(); + + int GetCount(InternalItemsQuery filter); + + QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery filter); + + QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery filter); + + QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery filter); + + QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery filter); + + QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery filter); + + QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery filter); + + IReadOnlyList<string> GetMusicGenreNames(); + + IReadOnlyList<string> GetStudioNames(); + + IReadOnlyList<string> GetGenreNames(); + + IReadOnlyList<string> GetAllArtistNames(); } diff --git a/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs b/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs new file mode 100644 index 000000000..6699d3a4d --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using Jellyfin.Data.Enums; +using MediaBrowser.Model.Querying; + +namespace MediaBrowser.Controller.Persistence; + +/// <summary> +/// Provides static lookup data for <see cref="ItemFields"/> and <see cref="BaseItemKind"/> for the domain. +/// </summary> +public interface IItemTypeLookup +{ + /// <summary> + /// Gets all serialisation target types for music related kinds. + /// </summary> + IReadOnlyList<string> MusicGenreTypes { get; } + + /// <summary> + /// Gets mapping for all BaseItemKinds and their expected serialization target. + /// </summary> + IReadOnlyDictionary<BaseItemKind, string> BaseItemKindNames { get; } +} diff --git a/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs b/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs new file mode 100644 index 000000000..2596784ba --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.MediaEncoding.Keyframes; + +namespace MediaBrowser.Controller.Persistence; + +/// <summary> +/// Provides methods for accessing keyframe data. +/// </summary> +public interface IKeyframeRepository +{ + /// <summary> + /// Gets the keyframe data. + /// </summary> + /// <param name="itemId">The item id.</param> + /// <returns>The keyframe data.</returns> + IReadOnlyList<KeyframeData> GetKeyframeData(Guid itemId); + + /// <summary> + /// Saves the keyframe data. + /// </summary> + /// <param name="itemId">The item id.</param> + /// <param name="data">The keyframe data.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The task object representing the asynchronous operation.</returns> + Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken); + + /// <summary> + /// Deletes the keyframe data. + /// </summary> + /// <param name="itemId">The item id.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The task object representing the asynchronous operation.</returns> + Task DeleteKeyframeDataAsync(Guid itemId, 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 +{ + /// <summary> + /// Gets the media attachments. + /// </summary> + /// <param name="filter">The query.</param> + /// <returns>IEnumerable{MediaAttachment}.</returns> + IReadOnlyList<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery filter); + + /// <summary> + /// Saves the media attachments. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="attachments">The attachments.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void SaveMediaAttachments(Guid id, IReadOnlyList<MediaAttachment> attachments, 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; + +/// <summary> +/// Provides methods for accessing MediaStreams. +/// </summary> +public interface IMediaStreamRepository +{ + /// <summary> + /// Gets the media streams. + /// </summary> + /// <param name="filter">The query.</param> + /// <returns>IEnumerable{MediaStream}.</returns> + IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery filter); + + /// <summary> + /// Saves the media streams. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="streams">The streams.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void SaveMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, CancellationToken cancellationToken); +} diff --git a/MediaBrowser.Controller/Persistence/IPeopleRepository.cs b/MediaBrowser.Controller/Persistence/IPeopleRepository.cs new file mode 100644 index 000000000..418289cb4 --- /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 +{ + /// <summary> + /// Gets the people. + /// </summary> + /// <param name="filter">The query.</param> + /// <returns>The list of people matching the filter.</returns> + IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery filter); + + /// <summary> + /// Updates the people. + /// </summary> + /// <param name="itemId">The item identifier.</param> + /// <param name="people">The people.</param> + void UpdatePeople(Guid itemId, IReadOnlyList<PersonInfo> people); + + /// <summary> + /// Gets the people names. + /// </summary> + /// <param name="filter">The query.</param> + /// <returns>The list of people names matching the filter.</returns> + IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery filter); +} diff --git a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs deleted file mode 100644 index f2fb2826a..000000000 --- a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs +++ /dev/null @@ -1,55 +0,0 @@ -#nullable disable - -using System; -using System.Collections.Generic; -using System.Threading; -using MediaBrowser.Controller.Entities; - -namespace MediaBrowser.Controller.Persistence -{ - /// <summary> - /// Provides an interface to implement a UserData repository. - /// </summary> - public interface IUserDataRepository : IDisposable - { - /// <summary> - /// Saves the user data. - /// </summary> - /// <param name="userId">The user id.</param> - /// <param name="key">The key.</param> - /// <param name="userData">The user data.</param> - /// <param name="cancellationToken">The cancellation token.</param> - void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken); - - /// <summary> - /// Gets the user data. - /// </summary> - /// <param name="userId">The user id.</param> - /// <param name="key">The key.</param> - /// <returns>The user data.</returns> - UserItemData GetUserData(long userId, string key); - - /// <summary> - /// Gets the user data. - /// </summary> - /// <param name="userId">The user id.</param> - /// <param name="keys">The keys.</param> - /// <returns>The user data.</returns> - UserItemData GetUserData(long userId, List<string> keys); - - /// <summary> - /// Return all user data associated with the given user. - /// </summary> - /// <param name="userId">The user id.</param> - /// <returns>The list of user item data.</returns> - List<UserItemData> GetAllUserData(long userId); - - /// <summary> - /// Save all user data associated with the given user. - /// </summary> - /// <param name="userId">The user id.</param> - /// <param name="userData">The user item data.</param> - /// <param name="cancellationToken">The cancellation token.</param> - void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken); - } -} diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index 45aefacf6..1062399e3 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -9,8 +9,10 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Data; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -137,27 +139,27 @@ namespace MediaBrowser.Controller.Playlists return Task.CompletedTask; } - public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + public override IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) { return GetPlayableItems(user, query); } - protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService) + protected override IReadOnlyList<BaseItem> GetNonCachedChildren(IDirectoryService directoryService) { return []; } - public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) + public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) { return GetPlayableItems(user, query); } - public IEnumerable<Tuple<LinkedChild, BaseItem>> GetManageableItems() + public IReadOnlyList<Tuple<LinkedChild, BaseItem>> GetManageableItems() { return GetLinkedChildrenInfos(); } - private List<BaseItem> GetPlayableItems(User user, InternalItemsQuery query) + private IReadOnlyList<BaseItem> GetPlayableItems(User user, InternalItemsQuery query) { query ??= new InternalItemsQuery(user); @@ -227,11 +229,11 @@ namespace MediaBrowser.Controller.Playlists return [item]; } - public override bool IsVisible(User user) + public override bool IsVisible(User user, bool skipAllowedTagsCheck = false) { if (!IsSharedItem) { - return base.IsVisible(user); + return base.IsVisible(user, skipAllowedTagsCheck); } if (OpenAccess) diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs index 474f09dc5..a1edfa3c9 100644 --- a/MediaBrowser.Controller/Providers/DirectoryService.cs +++ b/MediaBrowser.Controller/Providers/DirectoryService.cs @@ -10,14 +10,15 @@ namespace MediaBrowser.Controller.Providers { public class DirectoryService : IDirectoryService { - private readonly IFileSystem _fileSystem; - + // TODO make static and switch to FastConcurrentLru. private readonly ConcurrentDictionary<string, FileSystemMetadata[]> _cache = new(StringComparer.Ordinal); private readonly ConcurrentDictionary<string, FileSystemMetadata> _fileCache = new(StringComparer.Ordinal); private readonly ConcurrentDictionary<string, List<string>> _filePathCache = new(StringComparer.Ordinal); + private readonly IFileSystem _fileSystem; + public DirectoryService(IFileSystem fileSystem) { _fileSystem = fileSystem; diff --git a/MediaBrowser.Controller/Providers/IExternalId.cs b/MediaBrowser.Controller/Providers/IExternalId.cs index f451eac6d..584c3297a 100644 --- a/MediaBrowser.Controller/Providers/IExternalId.cs +++ b/MediaBrowser.Controller/Providers/IExternalId.cs @@ -32,12 +32,6 @@ namespace MediaBrowser.Controller.Providers ExternalIdMediaType? Type { get; } /// <summary> - /// Gets the URL format string for this id. - /// </summary> - [Obsolete("Obsolete in 10.10, to be removed in 10.11")] - string? UrlFormatString { get; } - - /// <summary> /// Determines whether this id supports a given item type. /// </summary> /// <param name="item">The item.</param> diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs index cfff3eb14..ef69885fc 100644 --- a/MediaBrowser.Controller/Providers/MetadataResult.cs +++ b/MediaBrowser.Controller/Providers/MetadataResult.cs @@ -3,6 +3,7 @@ #pragma warning disable CA1002, CA2227, CS1591 using System.Collections.Generic; +using System.Linq; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Entities; @@ -13,6 +14,7 @@ namespace MediaBrowser.Controller.Providers // Images aren't always used so the allocation is a waste a lot of the time private List<LocalImageInfo> _images; private List<(string Url, ImageType Type)> _remoteImages; + private List<PersonInfo> _people; public MetadataResult() { @@ -21,17 +23,21 @@ namespace MediaBrowser.Controller.Providers public List<LocalImageInfo> Images { - get => _images ??= new List<LocalImageInfo>(); + get => _images ??= []; set => _images = value; } public List<(string Url, ImageType Type)> RemoteImages { - get => _remoteImages ??= new List<(string Url, ImageType Type)>(); + get => _remoteImages ??= []; set => _remoteImages = value; } - public List<PersonInfo> People { get; set; } + public IReadOnlyList<PersonInfo> People + { + get => _people; + set => _people = value?.ToList(); + } public bool HasMetadata { get; set; } @@ -47,7 +53,7 @@ namespace MediaBrowser.Controller.Providers { People ??= new List<PersonInfo>(); - PeopleHelper.AddPerson(People, p); + PeopleHelper.AddPerson(_people, p); } /// <summary> @@ -61,7 +67,7 @@ namespace MediaBrowser.Controller.Providers } else { - People.Clear(); + _people.Clear(); } } } diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 462a62455..2b3afa117 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -6,7 +6,8 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities.Security; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Entities.Security; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; @@ -73,7 +74,7 @@ namespace MediaBrowser.Controller.Session /// <param name="remoteEndPoint">The remote end point.</param> /// <param name="user">The user.</param> /// <returns>A task containing the session information.</returns> - Task<SessionInfo> LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, Jellyfin.Data.Entities.User user); + Task<SessionInfo> LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user); /// <summary> /// Used to report that a session controller has connected. @@ -160,7 +161,7 @@ namespace MediaBrowser.Controller.Session /// <param name="sessionId">The identifier of the session.</param> /// <param name="command">The group update.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <typeparam name="T">Type of group.</typeparam> + /// <typeparam name="T">The group update type.</typeparam> /// <returns>Task.</returns> Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancellationToken); @@ -324,7 +325,7 @@ namespace MediaBrowser.Controller.Session Task<SessionInfo> GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, string appVersion); /// <summary> - /// Logouts the specified access token. + /// Logs out the specified access token. /// </summary> /// <param name="accessToken">The access token.</param> /// <returns>A <see cref="Task"/> representing the log out process.</returns> @@ -341,5 +342,13 @@ namespace MediaBrowser.Controller.Session Task RevokeUserTokens(Guid userId, string currentAccessToken); Task CloseIfNeededAsync(SessionInfo session); + + /// <summary> + /// Used to close the livestream if needed. + /// </summary> + /// <param name="liveStreamId">The livestream id.</param> + /// <param name="sessionIdOrPlaySessionId">The session id or playsession id.</param> + /// <returns>Task.</returns> + Task CloseLiveStreamIfNeededAsync(string liveStreamId, string sessionIdOrPlaySessionId); } } diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index cbef5d011..96783f607 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -286,7 +286,7 @@ namespace MediaBrowser.Controller.Session /// <summary> /// Gets or sets the playlist item id. /// </summary> - /// <value>The splaylist item id.</value> + /// <value>The playlist item id.</value> public string PlaylistItemId { get; set; } /// <summary> diff --git a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs index bd47db39a..2206a021a 100644 --- a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs +++ b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs @@ -1,11 +1,12 @@ #nullable disable +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Library; namespace MediaBrowser.Controller.Sorting { /// <summary> - /// Represents a BaseItem comparer that requires a User to perform it's comparison. + /// Represents a BaseItem comparer that requires a User to perform its comparison. /// </summary> public interface IUserBaseItemComparer : IBaseItemComparer { @@ -13,7 +14,7 @@ namespace MediaBrowser.Controller.Sorting /// Gets or sets the user. /// </summary> /// <value>The user.</value> - Jellyfin.Data.Entities.User User { get; set; } + User User { get; set; } /// <summary> /// Gets or sets the user manager. @@ -25,6 +26,6 @@ namespace MediaBrowser.Controller.Sorting /// Gets or sets the user data repository. /// </summary> /// <value>The user data repository.</value> - IUserDataManager UserDataRepository { get; set; } + IUserDataManager UserDataManager { get; set; } } } diff --git a/MediaBrowser.Controller/Streaming/StreamState.cs b/MediaBrowser.Controller/Streaming/StreamState.cs index b5dbe29ec..195dda5fe 100644 --- a/MediaBrowser.Controller/Streaming/StreamState.cs +++ b/MediaBrowser.Controller/Streaming/StreamState.cs @@ -51,7 +51,7 @@ public class StreamState : EncodingJobInfo, IDisposable public VideoRequestDto? VideoRequest => Request as VideoRequestDto; /// <summary> - /// Gets or sets the direct stream provicer. + /// Gets or sets the direct stream provider. /// </summary> /// <remarks> /// Deprecated. diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs index 51c95a1bb..31890c40a 100644 --- a/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs +++ b/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs @@ -80,7 +80,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates } var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.RemoveItems); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); if (playingItemRemoved && !context.PlayQueue.IsItemPlaying()) @@ -106,7 +106,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates } var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.MoveItem); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); } @@ -127,7 +127,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates _ => PlayQueueUpdateReason.Queue }; var playQueueUpdate = context.GetPlayQueueUpdate(reason); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); } @@ -184,7 +184,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates { context.SetRepeatMode(request.Mode); var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.RepeatMode); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); } @@ -193,7 +193,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates { context.SetShuffleMode(request.Mode); var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.ShuffleMode); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); } @@ -221,7 +221,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates { // Notify relevant state change event. var stateUpdate = new GroupStateUpdate(Type, reason.Action); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.StateUpdate, stateUpdate); + var update = new SyncPlayStateUpdate(context.GroupId, stateUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); } diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs index dcc06db1e..132765b71 100644 --- a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs +++ b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs @@ -78,7 +78,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates // Prepare new session. var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NewPlaylist); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, update, cancellationToken); context.SetBuffering(session, true); @@ -152,7 +152,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates } var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NewPlaylist); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); // Reset status of sessions and await for all Ready events. @@ -177,7 +177,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates if (result) { var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.SetCurrentItem); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); // Reset status of sessions and await for all Ready events. @@ -215,7 +215,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates context.RestartCurrentItem(); var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NewPlaylist); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); // Reset status of sessions and await for all Ready events. @@ -336,7 +336,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates _logger.LogDebug("Session {SessionId} reported wrong playlist item in group {GroupId}.", session.Id, context.GroupId.ToString()); var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.SetCurrentItem); - var updateSession = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var updateSession = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken); context.SetBuffering(session, true); @@ -410,7 +410,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates _logger.LogDebug("Session {SessionId} reported wrong playlist item in group {GroupId}.", session.Id, context.GroupId.ToString()); var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.SetCurrentItem); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, update, cancellationToken); context.SetBuffering(session, true); @@ -583,7 +583,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates { // Send playing-queue update. var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NextItem); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); // Reset status of sessions and await for all Ready events. @@ -629,7 +629,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates { // Send playing-queue update. var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.PreviousItem); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); // Reset status of sessions and await for all Ready events. diff --git a/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs b/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs index d2de22450..ddf86be71 100644 --- a/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs +++ b/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs @@ -66,11 +66,11 @@ namespace MediaBrowser.Controller.SyncPlay /// <summary> /// Sends a GroupUpdate message to the interested sessions. /// </summary> - /// <typeparam name="T">The type of the data of the message.</typeparam> /// <param name="from">The current session.</param> /// <param name="type">The filtering type.</param> /// <param name="message">The message to send.</param> /// <param name="cancellationToken">The cancellation token.</param> + /// <typeparam name="T">The group update type.</typeparam> /// <returns>The task.</returns> Task SendGroupUpdate<T>(SessionInfo from, SyncPlayBroadcastType type, GroupUpdate<T> message, CancellationToken cancellationToken); @@ -92,15 +92,6 @@ namespace MediaBrowser.Controller.SyncPlay SendCommand NewSyncPlayCommand(SendCommandType type); /// <summary> - /// Builds a new group update message. - /// </summary> - /// <typeparam name="T">The type of the data of the message.</typeparam> - /// <param name="type">The update type.</param> - /// <param name="data">The data to send.</param> - /// <returns>The group update.</returns> - GroupUpdate<T> NewSyncPlayGroupUpdate<T>(GroupUpdateType type, T data); - - /// <summary> /// Sanitizes the PositionTicks, considers the current playing item when available. /// </summary> /// <param name="positionTicks">The PositionTicks.</param> diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs index a6999a12c..6365a389e 100644 --- a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs +++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs @@ -20,7 +20,8 @@ namespace MediaBrowser.Controller.SyncPlay /// <param name="session">The session that's creating the group.</param> /// <param name="request">The request.</param> /// <param name="cancellationToken">The cancellation token.</param> - void NewGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken); + /// <returns>The newly created group.</returns> + GroupInfoDto NewGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken); /// <summary> /// Adds the session to a group. @@ -47,6 +48,14 @@ namespace MediaBrowser.Controller.SyncPlay List<GroupInfoDto> ListGroups(SessionInfo session, ListGroupsRequest request); /// <summary> + /// Gets available groups for a session by id. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="groupId">The group id.</param> + /// <returns>The groups or null.</returns> + GroupInfoDto GetGroup(SessionInfo session, Guid groupId); + + /// <summary> /// Handle a request by a session in a group. /// </summary> /// <param name="session">The session.</param> diff --git a/MediaBrowser.Controller/SystemBackupService/BackupManifestDto.cs b/MediaBrowser.Controller/SystemBackupService/BackupManifestDto.cs new file mode 100644 index 000000000..b094ec275 --- /dev/null +++ b/MediaBrowser.Controller/SystemBackupService/BackupManifestDto.cs @@ -0,0 +1,34 @@ +using System; + +namespace MediaBrowser.Controller.SystemBackupService; + +/// <summary> +/// Manifest type for backups internal structure. +/// </summary> +public class BackupManifestDto +{ + /// <summary> + /// Gets or sets the jellyfin version this backup was created with. + /// </summary> + public required Version ServerVersion { get; set; } + + /// <summary> + /// Gets or sets the backup engine version this backup was created with. + /// </summary> + public required Version BackupEngineVersion { get; set; } + + /// <summary> + /// Gets or sets the date this backup was created with. + /// </summary> + public required DateTimeOffset DateCreated { get; set; } + + /// <summary> + /// Gets or sets the path to the backup on the system. + /// </summary> + public required string Path { get; set; } + + /// <summary> + /// Gets or sets the contents of the backup archive. + /// </summary> + public required BackupOptionsDto Options { get; set; } +} diff --git a/MediaBrowser.Controller/SystemBackupService/BackupOptionsDto.cs b/MediaBrowser.Controller/SystemBackupService/BackupOptionsDto.cs new file mode 100644 index 000000000..fc5a109f1 --- /dev/null +++ b/MediaBrowser.Controller/SystemBackupService/BackupOptionsDto.cs @@ -0,0 +1,29 @@ +using System; + +namespace MediaBrowser.Controller.SystemBackupService; + +/// <summary> +/// Defines the optional contents of the backup archive. +/// </summary> +public class BackupOptionsDto +{ + /// <summary> + /// Gets or sets a value indicating whether the archive contains the Metadata contents. + /// </summary> + public bool Metadata { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the archive contains the Trickplay contents. + /// </summary> + public bool Trickplay { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the archive contains the Subtitle contents. + /// </summary> + public bool Subtitles { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the archive contains the Database contents. + /// </summary> + public bool Database { get; set; } = true; +} diff --git a/MediaBrowser.Controller/SystemBackupService/BackupRestoreRequestDto.cs b/MediaBrowser.Controller/SystemBackupService/BackupRestoreRequestDto.cs new file mode 100644 index 000000000..263fa00c8 --- /dev/null +++ b/MediaBrowser.Controller/SystemBackupService/BackupRestoreRequestDto.cs @@ -0,0 +1,15 @@ +using System; +using MediaBrowser.Common.Configuration; + +namespace MediaBrowser.Controller.SystemBackupService; + +/// <summary> +/// Defines properties used to start a restore process. +/// </summary> +public class BackupRestoreRequestDto +{ + /// <summary> + /// Gets or Sets the name of the backup archive to restore from. Must be present in <see cref="IApplicationPaths.BackupPath"/>. + /// </summary> + public required string ArchiveFileName { get; set; } +} diff --git a/MediaBrowser.Controller/SystemBackupService/IBackupService.cs b/MediaBrowser.Controller/SystemBackupService/IBackupService.cs new file mode 100644 index 000000000..0c586d811 --- /dev/null +++ b/MediaBrowser.Controller/SystemBackupService/IBackupService.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using MediaBrowser.Controller.SystemBackupService; + +namespace Jellyfin.Server.Implementations.SystemBackupService; + +/// <summary> +/// Defines an interface to restore and backup the jellyfin system. +/// </summary> +public interface IBackupService +{ + /// <summary> + /// Creates a new Backup zip file containing the current state of the application. + /// </summary> + /// <param name="backupOptions">The backup options.</param> + /// <returns>A task.</returns> + Task<BackupManifestDto> CreateBackupAsync(BackupOptionsDto backupOptions); + + /// <summary> + /// Gets a list of backups that are available to be restored from. + /// </summary> + /// <returns>A list of backup paths.</returns> + Task<BackupManifestDto[]> EnumerateBackups(); + + /// <summary> + /// Gets a single backup manifest if the path defines a valid Jellyfin backup archive. + /// </summary> + /// <param name="archivePath">The path to be loaded.</param> + /// <returns>The containing backup manifest or null if not existing or compatiable.</returns> + Task<BackupManifestDto?> GetBackupManifest(string archivePath); + + /// <summary> + /// Restores an backup zip file created by jellyfin. + /// </summary> + /// <param name="archivePath">Path to the archive.</param> + /// <returns>A Task.</returns> + /// <exception cref="FileNotFoundException">Thrown when an invalid or missing file is specified.</exception> + /// <exception cref="NotSupportedException">Thrown when attempt to load an unsupported backup is made.</exception> + /// <exception cref="InvalidOperationException">Thrown for errors during the restore.</exception> + Task RestoreBackupAsync(string archivePath); + + /// <summary> + /// Schedules a Restore and restarts the server. + /// </summary> + /// <param name="archivePath">The path to the archive to restore from.</param> + void ScheduleRestoreAndRestartServer(string archivePath); +} diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs index 800317800..fba24329a 100644 --- a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs +++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Configuration; @@ -21,7 +21,7 @@ public interface ITrickplayManager /// <param name="libraryOptions">The library options.</param> /// <param name="cancellationToken">CancellationToken to use for operation.</param> /// <returns>Task.</returns> - Task RefreshTrickplayDataAsync(Video video, bool replace, LibraryOptions? libraryOptions, CancellationToken cancellationToken); + Task RefreshTrickplayDataAsync(Video video, bool replace, LibraryOptions libraryOptions, CancellationToken cancellationToken); /// <summary> /// Creates trickplay tiles out of individual thumbnails. @@ -59,6 +59,14 @@ public interface ITrickplayManager Task SaveTrickplayInfo(TrickplayInfo info); /// <summary> + /// Deletes all trickplay info for an item. + /// </summary> + /// <param name="itemId">The item id.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task DeleteTrickplayDataAsync(Guid itemId, CancellationToken cancellationToken); + + /// <summary> /// Gets all trickplay infos for all media streams of an item. /// </summary> /// <param name="item">The item.</param> @@ -93,7 +101,7 @@ public interface ITrickplayManager /// <param name="libraryOptions">The library options.</param> /// <param name="cancellationToken">CancellationToken to use for operation.</param> /// <returns>Task.</returns> - Task MoveGeneratedTrickplayDataAsync(Video video, LibraryOptions? libraryOptions, CancellationToken cancellationToken); + Task MoveGeneratedTrickplayDataAsync(Video video, LibraryOptions libraryOptions, CancellationToken cancellationToken); /// <summary> /// Gets the trickplay HLS playlist. |
