diff options
Diffstat (limited to 'MediaBrowser.Controller')
332 files changed, 13104 insertions, 6940 deletions
diff --git a/MediaBrowser.Controller/Authentication/AuthenticationException.cs b/MediaBrowser.Controller/Authentication/AuthenticationException.cs new file mode 100644 index 000000000..081f877f7 --- /dev/null +++ b/MediaBrowser.Controller/Authentication/AuthenticationException.cs @@ -0,0 +1,35 @@ +using System; + +namespace MediaBrowser.Controller.Authentication +{ + /// <summary> + /// The exception that is thrown when an attempt to authenticate fails. + /// </summary> + public class AuthenticationException : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationException"/> class. + /// </summary> + public AuthenticationException() : base() + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationException"/> class. + /// </summary> + /// <param name="message">The message that describes the error.</param> + public AuthenticationException(string message) : base(message) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationException"/> class. + /// </summary> + /// <param name="message">The message that describes the error.</param> + /// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param> + public AuthenticationException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs index 3c65156e3..635e4eb3d 100644 --- a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs +++ b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs @@ -1,14 +1,20 @@ +#nullable disable + +#pragma warning disable CS1591 + using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; - namespace MediaBrowser.Controller.Authentication { public class AuthenticationResult { public UserDto User { get; set; } + public SessionInfo SessionInfo { get; set; } + public string AccessToken { get; set; } + public string ServerId { get; set; } } } diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs index b9f282bd2..a56d3c822 100644 --- a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs +++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs @@ -1,5 +1,9 @@ +#nullable disable + +#pragma warning disable CS1591 + using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; +using Jellyfin.Data.Entities; using MediaBrowser.Model.Users; namespace MediaBrowser.Controller.Authentication @@ -7,9 +11,13 @@ namespace MediaBrowser.Controller.Authentication public interface IAuthenticationProvider { string Name { get; } + bool IsEnabled { get; } + Task<ProviderAuthenticationResult> Authenticate(string username, string password); - Task<bool> HasPassword(User user); + + bool HasPassword(User user); + Task ChangePassword(User user, string newPassword); } @@ -26,6 +34,7 @@ namespace MediaBrowser.Controller.Authentication public class ProviderAuthenticationResult { public string Username { get; set; } + public string DisplayName { get; set; } } } diff --git a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs new file mode 100644 index 000000000..8c9d1baf8 --- /dev/null +++ b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs @@ -0,0 +1,29 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using MediaBrowser.Model.Users; + +namespace MediaBrowser.Controller.Authentication +{ + public interface IPasswordResetProvider + { + string Name { get; } + + bool IsEnabled { get; } + + Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork); + + Task<PinRedeemResult> RedeemPasswordResetPin(string pin); + } + + public class PasswordPinCreationResult + { + public string PinFile { get; set; } + + public DateTime ExpirationDate { get; set; } + } +} diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs new file mode 100644 index 000000000..abfdb41d8 --- /dev/null +++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs @@ -0,0 +1,137 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using Jellyfin.Extensions; +using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Configuration; + +namespace MediaBrowser.Controller.BaseItemManager +{ + /// <inheritdoc /> + public class BaseItemManager : IBaseItemManager + { + private readonly IServerConfigurationManager _serverConfigurationManager; + + private int _metadataRefreshConcurrency; + + /// <summary> + /// Initializes a new instance of the <see cref="BaseItemManager"/> class. + /// </summary> + /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + public BaseItemManager(IServerConfigurationManager serverConfigurationManager) + { + _serverConfigurationManager = serverConfigurationManager; + + _metadataRefreshConcurrency = GetMetadataRefreshConcurrency(); + SetupMetadataThrottler(); + + _serverConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated; + } + + /// <inheritdoc /> + public SemaphoreSlim MetadataRefreshThrottler { get; private set; } + + /// <inheritdoc /> + public bool IsMetadataFetcherEnabled(BaseItem baseItem, LibraryOptions libraryOptions, string name) + { + if (baseItem is Channel) + { + // Hack alert. + return true; + } + + if (baseItem.SourceType == SourceType.Channel) + { + // Hack alert. + return !baseItem.EnableMediaSourceDisplay; + } + + var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name); + if (typeOptions != null) + { + return typeOptions.MetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); + } + + if (!libraryOptions.EnableInternetProviders) + { + return false; + } + + var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase)); + + return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); + } + + /// <inheritdoc /> + public bool IsImageFetcherEnabled(BaseItem baseItem, LibraryOptions libraryOptions, string name) + { + if (baseItem is Channel) + { + // Hack alert. + return true; + } + + if (baseItem.SourceType == SourceType.Channel) + { + // Hack alert. + return !baseItem.EnableMediaSourceDisplay; + } + + var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name); + if (typeOptions != null) + { + return typeOptions.ImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); + } + + if (!libraryOptions.EnableInternetProviders) + { + return false; + } + + var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase)); + + return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); + } + + /// <summary> + /// Called when the configuration is updated. + /// It will refresh the metadata throttler if the relevant config changed. + /// </summary> + private void OnConfigurationUpdated(object? sender, EventArgs e) + { + int newMetadataRefreshConcurrency = GetMetadataRefreshConcurrency(); + if (_metadataRefreshConcurrency != newMetadataRefreshConcurrency) + { + _metadataRefreshConcurrency = newMetadataRefreshConcurrency; + SetupMetadataThrottler(); + } + } + + /// <summary> + /// Creates the metadata refresh throttler. + /// </summary> + [MemberNotNull(nameof(MetadataRefreshThrottler))] + private void SetupMetadataThrottler() + { + MetadataRefreshThrottler = new SemaphoreSlim(_metadataRefreshConcurrency); + } + + /// <summary> + /// Returns the metadata refresh concurrency. + /// </summary> + private int GetMetadataRefreshConcurrency() + { + var concurrency = _serverConfigurationManager.Configuration.LibraryMetadataRefreshConcurrency; + + if (concurrency <= 0) + { + concurrency = Environment.ProcessorCount; + } + + return concurrency; + } + } +} diff --git a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs new file mode 100644 index 000000000..e18994214 --- /dev/null +++ b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs @@ -0,0 +1,35 @@ +using System.Threading; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Configuration; + +namespace MediaBrowser.Controller.BaseItemManager +{ + /// <summary> + /// The <c>BaseItem</c> manager. + /// </summary> + public interface IBaseItemManager + { + /// <summary> + /// Gets the semaphore used to limit the amount of concurrent metadata refreshes. + /// </summary> + SemaphoreSlim MetadataRefreshThrottler { get; } + + /// <summary> + /// Is metadata fetcher enabled. + /// </summary> + /// <param name="baseItem">The base item.</param> + /// <param name="libraryOptions">The library options.</param> + /// <param name="name">The metadata fetcher name.</param> + /// <returns><c>true</c> if metadata fetcher is enabled, else false.</returns> + bool IsMetadataFetcherEnabled(BaseItem baseItem, LibraryOptions libraryOptions, string name); + + /// <summary> + /// Is image fetcher enabled. + /// </summary> + /// <param name="baseItem">The base item.</param> + /// <param name="libraryOptions">The library options.</param> + /// <param name="name">The image fetcher name.</param> + /// <returns><c>true</c> if image fetcher is enabled, else false.</returns> + bool IsImageFetcherEnabled(BaseItem baseItem, LibraryOptions libraryOptions, string name); + } +} diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs index adf03fb66..e6923b55c 100644 --- a/MediaBrowser.Controller/Channels/Channel.cs +++ b/MediaBrowser.Controller/Channels/Channel.cs @@ -1,27 +1,42 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; +using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Channels { public class Channel : Folder { + [JsonIgnore] + public override bool SupportsInheritedParentImages => false; + + [JsonIgnore] + public override SourceType SourceType => SourceType.Channel; + public override bool IsVisible(User user) { - if (user.Policy.BlockedChannels != null) + var blockedChannelsPreference = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels); + if (blockedChannelsPreference.Length != 0) { - if (user.Policy.BlockedChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) + if (blockedChannelsPreference.Contains(Id)) { return false; } } else { - if (!user.Policy.EnableAllChannels && !user.Policy.EnabledChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) + if (!user.HasPermission(PermissionKind.EnableAllChannels) + && !user.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels).Contains(Id)) { return false; } @@ -30,12 +45,6 @@ namespace MediaBrowser.Controller.Channels return base.IsVisible(user); } - [IgnoreDataMember] - public override bool SupportsInheritedParentImages => false; - - [IgnoreDataMember] - public override SourceType SourceType => SourceType.Channel; - protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) { try @@ -60,7 +69,7 @@ namespace MediaBrowser.Controller.Channels public static string GetInternalMetadataPath(string basePath, Guid id) { - return System.IO.Path.Combine(basePath, "channels", id.ToString("N"), "metadata"); + return System.IO.Path.Combine(basePath, "channels", id.ToString("N", CultureInfo.InvariantCulture), "metadata"); } public override bool CanDelete() @@ -75,7 +84,7 @@ namespace MediaBrowser.Controller.Channels internal static bool IsChannelVisible(BaseItem channelItem, User user) { - var channel = ChannelManager.GetChannel(channelItem.ChannelId.ToString("")); + var channel = ChannelManager.GetChannel(channelItem.ChannelId.ToString(string.Empty)); return channel.IsVisible(user); } diff --git a/MediaBrowser.Controller/Channels/ChannelItemInfo.cs b/MediaBrowser.Controller/Channels/ChannelItemInfo.cs index aff68883b..55f80b240 100644 --- a/MediaBrowser.Controller/Channels/ChannelItemInfo.cs +++ b/MediaBrowser.Controller/Channels/ChannelItemInfo.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CA1002, CA2227, CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Controller.Entities; @@ -9,6 +13,19 @@ namespace MediaBrowser.Controller.Channels { public class ChannelItemInfo : IHasProviderIds { + public ChannelItemInfo() + { + MediaSources = new List<MediaSourceInfo>(); + TrailerTypes = new List<TrailerType>(); + Genres = new List<string>(); + Studios = new List<string>(); + People = new List<PersonInfo>(); + Tags = new List<string>(); + ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + Artists = new List<string>(); + AlbumArtists = new List<string>(); + } + public string Name { get; set; } public string SeriesName { get; set; } @@ -24,7 +41,9 @@ namespace MediaBrowser.Controller.Channels public string Overview { get; set; } public List<string> Genres { get; set; } + public List<string> Studios { get; set; } + public List<string> Tags { get; set; } public List<PersonInfo> People { get; set; } @@ -34,26 +53,33 @@ namespace MediaBrowser.Controller.Channels public long? RunTimeTicks { get; set; } public string ImageUrl { get; set; } + public string OriginalTitle { get; set; } public ChannelMediaType MediaType { get; set; } + public ChannelFolderType FolderType { get; set; } public ChannelMediaContentType ContentType { get; set; } + public ExtraType ExtraType { get; set; } + public List<TrailerType> TrailerTypes { get; set; } public Dictionary<string, string> ProviderIds { get; set; } public DateTime? PremiereDate { get; set; } + public int? ProductionYear { get; set; } public DateTime? DateCreated { get; set; } public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } public int? IndexNumber { get; set; } + public int? ParentIndexNumber { get; set; } public List<MediaSourceInfo> MediaSources { get; set; } @@ -63,20 +89,9 @@ namespace MediaBrowser.Controller.Channels public List<string> Artists { get; set; } public List<string> AlbumArtists { get; set; } + public bool IsLiveStream { get; set; } - public string Etag { get; set; } - public ChannelItemInfo() - { - MediaSources = new List<MediaSourceInfo>(); - TrailerTypes = new List<TrailerType>(); - Genres = new List<string>(); - Studios = new List<string>(); - People = new List<PersonInfo>(); - Tags = new List<string>(); - ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - Artists = new List<string>(); - AlbumArtists = new List<string>(); - } + public string Etag { get; set; } } } diff --git a/MediaBrowser.Controller/Channels/ChannelItemResult.cs b/MediaBrowser.Controller/Channels/ChannelItemResult.cs index c194c8e48..ca7721991 100644 --- a/MediaBrowser.Controller/Channels/ChannelItemResult.cs +++ b/MediaBrowser.Controller/Channels/ChannelItemResult.cs @@ -1,16 +1,19 @@ +#pragma warning disable CS1591 + +using System; using System.Collections.Generic; namespace MediaBrowser.Controller.Channels { public class ChannelItemResult { - public List<ChannelItemInfo> Items { get; set; } - - public int? TotalRecordCount { get; set; } - public ChannelItemResult() { - Items = new List<ChannelItemInfo>(); + Items = Array.Empty<ChannelItemInfo>(); } + + public IReadOnlyList<ChannelItemInfo> Items { get; set; } + + public int? TotalRecordCount { get; set; } } } diff --git a/MediaBrowser.Controller/Channels/ChannelItemType.cs b/MediaBrowser.Controller/Channels/ChannelItemType.cs index 833ae75fe..3ce920e23 100644 --- a/MediaBrowser.Controller/Channels/ChannelItemType.cs +++ b/MediaBrowser.Controller/Channels/ChannelItemType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Channels { public enum ChannelItemType diff --git a/MediaBrowser.Controller/Channels/ChannelLatestMediaSearch.cs b/MediaBrowser.Controller/Channels/ChannelLatestMediaSearch.cs new file mode 100644 index 000000000..6f0761e64 --- /dev/null +++ b/MediaBrowser.Controller/Channels/ChannelLatestMediaSearch.cs @@ -0,0 +1,11 @@ +#nullable disable + +#pragma warning disable CS1591 + +namespace MediaBrowser.Controller.Channels +{ + public class ChannelLatestMediaSearch + { + public string UserId { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Channels/ChannelParentalRating.cs b/MediaBrowser.Controller/Channels/ChannelParentalRating.cs index 1d189446d..f77d81c16 100644 --- a/MediaBrowser.Controller/Channels/ChannelParentalRating.cs +++ b/MediaBrowser.Controller/Channels/ChannelParentalRating.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Channels { public enum ChannelParentalRating diff --git a/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs b/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs index c02055b90..990b025bc 100644 --- a/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs +++ b/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Channels { public class ChannelSearchInfo @@ -6,9 +10,4 @@ namespace MediaBrowser.Controller.Channels public string UserId { get; set; } } - - public class ChannelLatestMediaSearch - { - public string UserId { get; set; } - } } diff --git a/MediaBrowser.Controller/Channels/IChannel.cs b/MediaBrowser.Controller/Channels/IChannel.cs index f8ed98a45..01bf8d5c8 100644 --- a/MediaBrowser.Controller/Channels/IChannel.cs +++ b/MediaBrowser.Controller/Channels/IChannel.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -64,7 +68,7 @@ namespace MediaBrowser.Controller.Channels /// </summary> /// <param name="type">The type.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{DynamicImageInfo}.</returns> + /// <returns>Task{DynamicImageResponse}.</returns> Task<DynamicImageResponse> GetChannelImage(ImageType type, CancellationToken cancellationToken); /// <summary> diff --git a/MediaBrowser.Controller/Channels/IChannelManager.cs b/MediaBrowser.Controller/Channels/IChannelManager.cs index df508fbe2..49be897ef 100644 --- a/MediaBrowser.Controller/Channels/IChannelManager.cs +++ b/MediaBrowser.Controller/Channels/IChannelManager.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading; @@ -22,7 +26,7 @@ namespace MediaBrowser.Controller.Channels /// </summary> /// <param name="id">The identifier.</param> /// <returns>ChannelFeatures.</returns> - ChannelFeatures GetChannelFeatures(string id); + ChannelFeatures GetChannelFeatures(Guid? id); /// <summary> /// Gets all channel features. @@ -31,6 +35,7 @@ namespace MediaBrowser.Controller.Channels ChannelFeatures[] GetAllChannelFeatures(); bool EnableMediaSourceDisplay(BaseItem item); + bool CanDelete(BaseItem item); Task DeleteItem(BaseItem item); @@ -46,32 +51,47 @@ namespace MediaBrowser.Controller.Channels /// Gets the channels internal. /// </summary> /// <param name="query">The query.</param> + /// <returns>The channels.</returns> QueryResult<Channel> GetChannelsInternal(ChannelQuery query); /// <summary> /// Gets the channels. /// </summary> /// <param name="query">The query.</param> + /// <returns>The channels.</returns> QueryResult<BaseItemDto> GetChannels(ChannelQuery query); /// <summary> - /// Gets the latest media. + /// Gets the latest channel items. /// </summary> + /// <param name="query">The item query.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The latest channels.</returns> Task<QueryResult<BaseItemDto>> GetLatestChannelItems(InternalItemsQuery query, CancellationToken cancellationToken); /// <summary> - /// Gets the latest media. + /// Gets the latest channel items. /// </summary> + /// <param name="query">The item query.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The latest channels.</returns> Task<QueryResult<BaseItem>> GetLatestChannelItemsInternal(InternalItemsQuery query, CancellationToken cancellationToken); /// <summary> /// Gets the channel items. /// </summary> + /// <param name="query">The query.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The channel items.</returns> Task<QueryResult<BaseItemDto>> GetChannelItems(InternalItemsQuery query, CancellationToken cancellationToken); /// <summary> - /// Gets the channel items internal. + /// Gets the channel items. /// </summary> + /// <param name="query">The query.</param> + /// <param name="progress">The progress to report to.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The channel items.</returns> Task<QueryResult<BaseItem>> GetChannelItemsInternal(InternalItemsQuery query, IProgress<double> progress, CancellationToken cancellationToken); /// <summary> @@ -79,9 +99,14 @@ namespace MediaBrowser.Controller.Channels /// </summary> /// <param name="item">The item.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{IEnumerable{MediaSourceInfo}}.</returns> + /// <returns>The item media sources.</returns> IEnumerable<MediaSourceInfo> GetStaticMediaSources(BaseItem item, CancellationToken cancellationToken); + /// <summary> + /// Whether the item supports media probe. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>Whether media probe should be enabled.</returns> bool EnableMediaProbe(BaseItem item); } } diff --git a/MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs b/MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs new file mode 100644 index 000000000..0539b9048 --- /dev/null +++ b/MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs @@ -0,0 +1,12 @@ +namespace MediaBrowser.Controller.Channels +{ + /// <summary> + /// Disable media source display. + /// </summary> + /// <remarks> + /// <see cref="Channel"/> can inherit this interface to disable being displayed. + /// </remarks> + public interface IDisableMediaSourceDisplay + { + } +} diff --git a/MediaBrowser.Controller/Channels/IHasCacheKey.cs b/MediaBrowser.Controller/Channels/IHasCacheKey.cs index d86ad0dba..9fae43033 100644 --- a/MediaBrowser.Controller/Channels/IHasCacheKey.cs +++ b/MediaBrowser.Controller/Channels/IHasCacheKey.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Channels { public interface IHasCacheKey diff --git a/MediaBrowser.Controller/Channels/IHasFolderAttributes.cs b/MediaBrowser.Controller/Channels/IHasFolderAttributes.cs new file mode 100644 index 000000000..64af8496c --- /dev/null +++ b/MediaBrowser.Controller/Channels/IHasFolderAttributes.cs @@ -0,0 +1,9 @@ +#pragma warning disable CA1819, CS1591 + +namespace MediaBrowser.Controller.Channels +{ + public interface IHasFolderAttributes + { + string[] Attributes { get; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs b/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs index deee46ff7..eeaa6b622 100644 --- a/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs +++ b/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs @@ -5,11 +5,17 @@ using MediaBrowser.Model.Dto; namespace MediaBrowser.Controller.Channels { + /// <summary> + /// The channel requires a media info callback. + /// </summary> public interface IRequiresMediaInfoCallback { /// <summary> /// Gets the channel item media information. /// </summary> + /// <param name="id">The channel item id.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The enumerable of media source info.</returns> Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaInfo(string id, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Channels/ISearchableChannel.cs b/MediaBrowser.Controller/Channels/ISearchableChannel.cs index d5b76a160..b87943a6e 100644 --- a/MediaBrowser.Controller/Channels/ISearchableChannel.cs +++ b/MediaBrowser.Controller/Channels/ISearchableChannel.cs @@ -1,7 +1,10 @@ +#nullable disable + +#pragma warning disable CS1591 + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Channels { @@ -15,36 +18,4 @@ namespace MediaBrowser.Controller.Channels /// <returns>Task{IEnumerable{ChannelItemInfo}}.</returns> Task<IEnumerable<ChannelItemInfo>> Search(ChannelSearchInfo searchInfo, CancellationToken cancellationToken); } - - public interface ISupportsLatestMedia - { - /// <summary> - /// Gets the latest media. - /// </summary> - /// <param name="request">The request.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{IEnumerable{ChannelItemInfo}}.</returns> - Task<IEnumerable<ChannelItemInfo>> GetLatestMedia(ChannelLatestMediaSearch request, CancellationToken cancellationToken); - } - - public interface ISupportsDelete - { - bool CanDelete(BaseItem item); - Task DeleteItem(string id, CancellationToken cancellationToken); - } - - public interface IDisableMediaSourceDisplay - { - - } - - public interface ISupportsMediaProbe - { - - } - - public interface IHasFolderAttributes - { - string[] Attributes { get; } - } } diff --git a/MediaBrowser.Controller/Channels/ISupportsDelete.cs b/MediaBrowser.Controller/Channels/ISupportsDelete.cs new file mode 100644 index 000000000..204054374 --- /dev/null +++ b/MediaBrowser.Controller/Channels/ISupportsDelete.cs @@ -0,0 +1,15 @@ +#pragma warning disable CS1591 + +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Channels +{ + public interface ISupportsDelete + { + bool CanDelete(BaseItem item); + + Task DeleteItem(string id, CancellationToken cancellationToken); + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs b/MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs new file mode 100644 index 000000000..dbba7cba2 --- /dev/null +++ b/MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs @@ -0,0 +1,21 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Channels +{ + public interface ISupportsLatestMedia + { + /// <summary> + /// Gets the latest media. + /// </summary> + /// <param name="request">The request.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The latest media.</returns> + Task<IEnumerable<ChannelItemInfo>> GetLatestMedia(ChannelLatestMediaSearch request, CancellationToken cancellationToken); + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs b/MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs new file mode 100644 index 000000000..bc7683125 --- /dev/null +++ b/MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs @@ -0,0 +1,9 @@ +namespace MediaBrowser.Controller.Channels +{ + /// <summary> + /// Channel supports media probe. + /// </summary> + public interface ISupportsMediaProbe + { + } +} diff --git a/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs index 60455e68a..394996868 100644 --- a/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs +++ b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CA1002, CA2227, CS1591 + using System.Collections.Generic; using MediaBrowser.Model.Channels; @@ -5,6 +9,14 @@ namespace MediaBrowser.Controller.Channels { public class InternalChannelFeatures { + public InternalChannelFeatures() + { + MediaTypes = new List<ChannelMediaType>(); + ContentTypes = new List<ChannelMediaContentType>(); + + DefaultSortFields = new List<ChannelItemSortField>(); + } + /// <summary> /// Gets or sets the media types. /// </summary> @@ -18,7 +30,7 @@ namespace MediaBrowser.Controller.Channels public List<ChannelMediaContentType> ContentTypes { get; set; } /// <summary> - /// Represents the maximum number of records the channel allows retrieving at a time + /// Gets or sets the maximum number of records the channel allows retrieving at a time. /// </summary> public int? MaxPageSize { get; set; } @@ -29,9 +41,10 @@ namespace MediaBrowser.Controller.Channels public List<ChannelItemSortField> DefaultSortFields { get; set; } /// <summary> - /// Indicates if a sort ascending/descending toggle is supported or not. + /// Gets or sets a value indicating whether a sort ascending/descending toggle is supported or not. /// </summary> public bool SupportsSortOrderToggle { get; set; } + /// <summary> /// Gets or sets the automatic refresh levels. /// </summary> @@ -43,18 +56,11 @@ namespace MediaBrowser.Controller.Channels /// </summary> /// <value>The daily download limit.</value> public int? DailyDownloadLimit { get; set; } + /// <summary> /// Gets or sets a value indicating whether [supports downloading]. /// </summary> /// <value><c>true</c> if [supports downloading]; otherwise, <c>false</c>.</value> public bool SupportsContentDownloading { get; set; } - - public InternalChannelFeatures() - { - MediaTypes = new List<ChannelMediaType>(); - ContentTypes = new List<ChannelMediaContentType>(); - - DefaultSortFields = new List<ChannelItemSortField>(); - } } } diff --git a/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs b/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs index 6d5ca75af..0d837faca 100644 --- a/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs +++ b/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs @@ -1,7 +1,10 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Channels; - namespace MediaBrowser.Controller.Channels { public class InternalChannelItemQuery diff --git a/MediaBrowser.Controller/Chapters/IChapterManager.cs b/MediaBrowser.Controller/Chapters/IChapterManager.cs index d061898a1..c049bb97e 100644 --- a/MediaBrowser.Controller/Chapters/IChapterManager.cs +++ b/MediaBrowser.Controller/Chapters/IChapterManager.cs @@ -1,16 +1,19 @@ +using System; using System.Collections.Generic; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Chapters { /// <summary> - /// Interface IChapterManager + /// Interface IChapterManager. /// </summary> public interface IChapterManager { /// <summary> /// Saves the chapters. /// </summary> - void SaveChapters(string itemId, List<ChapterInfo> chapters); + /// <param name="itemId">The item.</param> + /// <param name="chapters">The set of chapters.</param> + void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters); } } diff --git a/MediaBrowser.Controller/Collections/CollectionCreatedEventArgs.cs b/MediaBrowser.Controller/Collections/CollectionCreatedEventArgs.cs new file mode 100644 index 000000000..82b3a4977 --- /dev/null +++ b/MediaBrowser.Controller/Collections/CollectionCreatedEventArgs.cs @@ -0,0 +1,24 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; +using MediaBrowser.Controller.Entities.Movies; + +namespace MediaBrowser.Controller.Collections +{ + public class CollectionCreatedEventArgs : EventArgs + { + /// <summary> + /// Gets or sets the collection. + /// </summary> + /// <value>The collection.</value> + public BoxSet Collection { get; set; } + + /// <summary> + /// Gets or sets the options. + /// </summary> + /// <value>The options.</value> + public CollectionCreationOptions Options { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs b/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs index 51fe4ce29..30f5f4efa 100644 --- a/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs +++ b/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.Entities; @@ -6,6 +10,13 @@ namespace MediaBrowser.Controller.Collections { public class CollectionCreationOptions : IHasProviderIds { + public CollectionCreationOptions() + { + ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + ItemIdList = Array.Empty<string>(); + UserIds = Array.Empty<Guid>(); + } + public string Name { get; set; } public Guid? ParentId { get; set; } @@ -14,14 +25,8 @@ namespace MediaBrowser.Controller.Collections public Dictionary<string, string> ProviderIds { get; set; } - public string[] ItemIdList { get; set; } - public Guid[] UserIds { get; set; } + public IReadOnlyList<string> ItemIdList { get; set; } - public CollectionCreationOptions() - { - ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - ItemIdList = Array.Empty<string>(); - UserIds = Array.Empty<Guid>(); - } + public IReadOnlyList<Guid> UserIds { get; set; } } } diff --git a/MediaBrowser.Controller/Collections/CollectionEvents.cs b/MediaBrowser.Controller/Collections/CollectionModifiedEventArgs.cs index bfc6af463..e538fa4b3 100644 --- a/MediaBrowser.Controller/Collections/CollectionEvents.cs +++ b/MediaBrowser.Controller/Collections/CollectionModifiedEventArgs.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Controller.Entities; @@ -5,23 +7,14 @@ using MediaBrowser.Controller.Entities.Movies; namespace MediaBrowser.Controller.Collections { - public class CollectionCreatedEventArgs : EventArgs - { - /// <summary> - /// Gets or sets the collection. - /// </summary> - /// <value>The collection.</value> - public BoxSet Collection { get; set; } - - /// <summary> - /// Gets or sets the options. - /// </summary> - /// <value>The options.</value> - public CollectionCreationOptions Options { get; set; } - } - public class CollectionModifiedEventArgs : EventArgs { + public CollectionModifiedEventArgs(BoxSet collection, IReadOnlyCollection<BaseItem> itemsChanged) + { + Collection = collection; + ItemsChanged = itemsChanged; + } + /// <summary> /// Gets or sets the collection. /// </summary> @@ -32,6 +25,6 @@ namespace MediaBrowser.Controller.Collections /// Gets or sets the items changed. /// </summary> /// <value>The items changed.</value> - public List<BaseItem> ItemsChanged { get; set; } + public IReadOnlyCollection<BaseItem> ItemsChanged { get; set; } } } diff --git a/MediaBrowser.Controller/Collections/ICollectionManager.cs b/MediaBrowser.Controller/Collections/ICollectionManager.cs index cfe8493d3..b8c33ee5a 100644 --- a/MediaBrowser.Controller/Collections/ICollectionManager.cs +++ b/MediaBrowser.Controller/Collections/ICollectionManager.cs @@ -1,5 +1,9 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; @@ -10,40 +14,40 @@ namespace MediaBrowser.Controller.Collections /// <summary> /// Occurs when [collection created]. /// </summary> - event EventHandler<CollectionCreatedEventArgs> CollectionCreated; + event EventHandler<CollectionCreatedEventArgs>? CollectionCreated; /// <summary> /// Occurs when [items added to collection]. /// </summary> - event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection; + event EventHandler<CollectionModifiedEventArgs>? ItemsAddedToCollection; /// <summary> /// Occurs when [items removed from collection]. /// </summary> - event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection; + event EventHandler<CollectionModifiedEventArgs>? ItemsRemovedFromCollection; /// <summary> /// Creates the collection. /// </summary> /// <param name="options">The options.</param> - BoxSet CreateCollection(CollectionCreationOptions options); + /// <returns>BoxSet wrapped in an awaitable task.</returns> + Task<BoxSet> CreateCollectionAsync(CollectionCreationOptions options); /// <summary> /// Adds to collection. /// </summary> /// <param name="collectionId">The collection identifier.</param> /// <param name="itemIds">The item ids.</param> - void AddToCollection(Guid collectionId, IEnumerable<string> itemIds); + /// <returns><see cref="Task"/> representing the asynchronous operation.</returns> + Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds); /// <summary> /// Removes from collection. /// </summary> /// <param name="collectionId">The collection identifier.</param> /// <param name="itemIds">The item ids.</param> - void RemoveFromCollection(Guid collectionId, IEnumerable<string> itemIds); - - void AddToCollection(Guid collectionId, IEnumerable<Guid> itemIds); - void RemoveFromCollection(Guid collectionId, IEnumerable<Guid> itemIds); + /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> + Task RemoveFromCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds); /// <summary> /// Collapses the items within box sets. diff --git a/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs b/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs index 6660743e6..43ad04dba 100644 --- a/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs +++ b/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs @@ -4,7 +4,7 @@ using MediaBrowser.Model.Configuration; namespace MediaBrowser.Controller.Configuration { /// <summary> - /// Interface IServerConfigurationManager + /// Interface IServerConfigurationManager. /// </summary> public interface IServerConfigurationManager : IConfigurationManager { @@ -19,7 +19,5 @@ namespace MediaBrowser.Controller.Configuration /// </summary> /// <value>The configuration.</value> ServerConfiguration Configuration { get; } - - bool SetOptimalValues(); } } diff --git a/MediaBrowser.Controller/Devices/CameraImageUploadInfo.cs b/MediaBrowser.Controller/Devices/CameraImageUploadInfo.cs deleted file mode 100644 index 89d0be58f..000000000 --- a/MediaBrowser.Controller/Devices/CameraImageUploadInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -using MediaBrowser.Model.Devices; - -namespace MediaBrowser.Controller.Devices -{ - public class CameraImageUploadInfo - { - public LocalFileInfo FileInfo { get; set; } - public DeviceInfo Device { get; set; } - } -} diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index 77d567631..8362db1a7 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -1,9 +1,14 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; -using System.IO; using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Entities.Security; +using Jellyfin.Data.Events; +using Jellyfin.Data.Queries; using MediaBrowser.Model.Devices; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; @@ -11,63 +16,65 @@ namespace MediaBrowser.Controller.Devices { public interface IDeviceManager { + event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated; + /// <summary> - /// Occurs when [camera image uploaded]. + /// Creates a new device. /// </summary> - event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded; + /// <param name="device">The device to create.</param> + /// <returns>A <see cref="Task{Device}"/> representing the creation of the device.</returns> + Task<Device> CreateDevice(Device device); /// <summary> /// Saves the capabilities. /// </summary> - /// <param name="reportedId">The reported identifier.</param> + /// <param name="deviceId">The device id.</param> /// <param name="capabilities">The capabilities.</param> - /// <returns>Task.</returns> - void SaveCapabilities(string reportedId, ClientCapabilities capabilities); + void SaveCapabilities(string deviceId, ClientCapabilities capabilities); /// <summary> /// Gets the capabilities. /// </summary> - /// <param name="reportedId">The reported identifier.</param> + /// <param name="deviceId">The device id.</param> /// <returns>ClientCapabilities.</returns> - ClientCapabilities GetCapabilities(string reportedId); + ClientCapabilities GetCapabilities(string deviceId); /// <summary> /// Gets the device information. /// </summary> /// <param name="id">The identifier.</param> /// <returns>DeviceInfo.</returns> - DeviceInfo GetDevice(string id); + Task<DeviceInfo> GetDevice(string id); /// <summary> - /// Gets the devices. + /// Gets devices based on the provided query. /// </summary> - /// <param name="query">The query.</param> - /// <returns>IEnumerable<DeviceInfo>.</returns> - QueryResult<DeviceInfo> GetDevices(DeviceQuery query); + /// <param name="query">The device query.</param> + /// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the devices.</returns> + Task<QueryResult<Device>> GetDevices(DeviceQuery query); - /// <summary> - /// Gets the upload history. - /// </summary> - /// <param name="deviceId">The device identifier.</param> - /// <returns>ContentUploadHistory.</returns> - ContentUploadHistory GetCameraUploadHistory(string deviceId); + Task<QueryResult<DeviceInfo>> GetDeviceInfos(DeviceQuery query); /// <summary> - /// Accepts the upload. + /// Gets the devices. /// </summary> - /// <param name="deviceId">The device identifier.</param> - /// <param name="stream">The stream.</param> - /// <param name="file">The file.</param> - /// <returns>Task.</returns> - Task AcceptCameraUpload(string deviceId, Stream stream, LocalFileInfo file); + /// <param name="userId">The user's id, or <c>null</c>.</param> + /// <param name="supportsSync">A value indicating whether the device supports sync, or <c>null</c>.</param> + /// <returns>IEnumerable<DeviceInfo>.</returns> + Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId, bool? supportsSync); + + Task DeleteDevice(Device device); /// <summary> /// Determines whether this instance [can access device] the specified user identifier. /// </summary> + /// <param name="user">The user to test.</param> + /// <param name="deviceId">The device id to test.</param> + /// <returns>Whether the user can access the device.</returns> bool CanAccessDevice(User user, string deviceId); - void UpdateDeviceOptions(string deviceId, DeviceOptions options); - DeviceOptions GetDeviceOptions(string deviceId); - event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated; + Task UpdateDeviceOptions(string deviceId, string deviceName); + + Task<DeviceOptions> GetDeviceOptions(string deviceId); } } diff --git a/MediaBrowser.Controller/Dlna/IDlnaManager.cs b/MediaBrowser.Controller/Dlna/IDlnaManager.cs index a6ee7c505..06da5ea09 100644 --- a/MediaBrowser.Controller/Dlna/IDlnaManager.cs +++ b/MediaBrowser.Controller/Dlna/IDlnaManager.cs @@ -1,6 +1,9 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.Dlna; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Dlna { @@ -17,7 +20,7 @@ namespace MediaBrowser.Controller.Dlna /// </summary> /// <param name="headers">The headers.</param> /// <returns>DeviceProfile.</returns> - DeviceProfile GetProfile(IDictionary<string, string> headers); + DeviceProfile? GetProfile(IHeaderDictionary headers); /// <summary> /// Gets the default profile. @@ -34,8 +37,9 @@ namespace MediaBrowser.Controller.Dlna /// <summary> /// Updates the profile. /// </summary> + /// <param name="profileId">The profile id.</param> /// <param name="profile">The profile.</param> - void UpdateProfile(DeviceProfile profile); + void UpdateProfile(string profileId, DeviceProfile profile); /// <summary> /// Deletes the profile. @@ -48,14 +52,14 @@ namespace MediaBrowser.Controller.Dlna /// </summary> /// <param name="id">The identifier.</param> /// <returns>DeviceProfile.</returns> - DeviceProfile GetProfile(string id); + DeviceProfile? GetProfile(string id); /// <summary> /// Gets the profile. /// </summary> /// <param name="deviceInfo">The device information.</param> /// <returns>DeviceProfile.</returns> - DeviceProfile GetProfile(DeviceIdentification deviceInfo); + DeviceProfile? GetProfile(DeviceIdentification deviceInfo); /// <summary> /// Gets the server description XML. @@ -64,13 +68,13 @@ namespace MediaBrowser.Controller.Dlna /// <param name="serverUuId">The server uu identifier.</param> /// <param name="serverAddress">The server address.</param> /// <returns>System.String.</returns> - string GetServerDescriptionXml(IDictionary<string, string> headers, string serverUuId, string serverAddress); + string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress); /// <summary> /// Gets the icon. /// </summary> /// <param name="filename">The filename.</param> /// <returns>DlnaIconResponse.</returns> - ImageStream GetIcon(string filename); + ImageStream? GetIcon(string filename); } } diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs index 4eaecd0a0..4e67cfee4 100644 --- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.Drawing; @@ -11,6 +13,7 @@ namespace MediaBrowser.Controller.Drawing /// </summary> /// <value>The supported input formats.</value> IReadOnlyCollection<string> SupportedInputFormats { get; } + /// <summary> /// Gets the supported output formats. /// </summary> @@ -18,19 +21,9 @@ namespace MediaBrowser.Controller.Drawing IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; } /// <summary> - /// Encodes the image. - /// </summary> - string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat); - - /// <summary> - /// Creates the image collage. - /// </summary> - /// <param name="options">The options.</param> - void CreateImageCollage(ImageCollageOptions options); - /// <summary> - /// Gets the name. + /// Gets the display name for the encoder. /// </summary> - /// <value>The name.</value> + /// <value>The display name.</value> string Name { get; } /// <summary> @@ -45,6 +38,41 @@ namespace MediaBrowser.Controller.Drawing /// <value><c>true</c> if [supports image encoding]; otherwise, <c>false</c>.</value> bool SupportsImageEncoding { get; } + /// <summary> + /// Get the dimensions of an image from the filesystem. + /// </summary> + /// <param name="path">The filepath of the image.</param> + /// <returns>The image dimensions.</returns> ImageDimensions GetImageSize(string path); + + /// <summary> + /// Gets the blurhash of an image. + /// </summary> + /// <param name="xComp">Amount of X components of DCT to take.</param> + /// <param name="yComp">Amount of Y components of DCT to take.</param> + /// <param name="path">The filepath of the image.</param> + /// <returns>The blurhash.</returns> + string GetImageBlurHash(int xComp, int yComp, string path); + + /// <summary> + /// Encode an image. + /// </summary> + /// <param name="inputPath">Input path of image.</param> + /// <param name="dateModified">Date modified.</param> + /// <param name="outputPath">Output path of image.</param> + /// <param name="autoOrient">Auto-orient image.</param> + /// <param name="orientation">Desired orientation of image.</param> + /// <param name="quality">Quality of encoded image.</param> + /// <param name="options">Image processing options.</param> + /// <param name="outputFormat">Image format of output.</param> + /// <returns>Path of encoded image.</returns> + string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat); + + /// <summary> + /// Create an image collage. + /// </summary> + /// <param name="options">The options to use when creating the collage.</param> + /// <param name="libraryName">Optional. </param> + void CreateImageCollage(ImageCollageOptions options, string? libraryName); } } diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index a11e2186f..7ca0e851b 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -1,16 +1,18 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Drawing { /// <summary> - /// Interface IImageProcessor + /// Interface IImageProcessor. /// </summary> public interface IImageProcessor { @@ -21,16 +23,16 @@ namespace MediaBrowser.Controller.Drawing IReadOnlyCollection<string> SupportedInputFormats { get; } /// <summary> - /// Gets the image enhancers. + /// Gets a value indicating whether [supports image collage creation]. /// </summary> - /// <value>The image enhancers.</value> - IImageEnhancer[] ImageEnhancers { get; } + /// <value><c>true</c> if [supports image collage creation]; otherwise, <c>false</c>.</value> + bool SupportsImageCollageCreation { get; } /// <summary> /// Gets the dimensions of the image. /// </summary> /// <param name="path">Path to the image file.</param> - /// <returns>ImageDimensions</returns> + /// <returns>ImageDimensions.</returns> ImageDimensions GetImageDimensions(string path); /// <summary> @@ -38,31 +40,15 @@ namespace MediaBrowser.Controller.Drawing /// </summary> /// <param name="item">The base item.</param> /// <param name="info">The information.</param> - /// <returns>ImageDimensions</returns> + /// <returns>ImageDimensions.</returns> ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info); /// <summary> - /// Gets the dimensions of the image. + /// Gets the blurhash of the image. /// </summary> - /// <param name="item">The base item.</param> - /// <param name="info">The information.</param> - /// <param name="updateItem">Whether or not the item info should be updated.</param> - /// <returns>ImageDimensions</returns> - ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info, bool updateItem); - - /// <summary> - /// Adds the parts. - /// </summary> - /// <param name="enhancers">The enhancers.</param> - void AddParts(IEnumerable<IImageEnhancer> enhancers); - - /// <summary> - /// Gets the supported enhancers. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="imageType">Type of the image.</param> - /// <returns>IEnumerable{IImageEnhancer}.</returns> - IImageEnhancer[] GetSupportedEnhancers(BaseItem item, ImageType imageType); + /// <param name="path">Path to the image file.</param> + /// <returns>BlurHash.</returns> + string GetImageBlurHash(string path); /// <summary> /// Gets the image cache tag. @@ -71,16 +57,10 @@ namespace MediaBrowser.Controller.Drawing /// <param name="image">The image.</param> /// <returns>Guid.</returns> string GetImageCacheTag(BaseItem item, ItemImageInfo image); - string GetImageCacheTag(BaseItem item, ChapterInfo info); - /// <summary> - /// Gets the image cache tag. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="image">The image.</param> - /// <param name="imageEnhancers">The image enhancers.</param> - /// <returns>Guid.</returns> - string GetImageCacheTag(BaseItem item, ItemImageInfo image, IImageEnhancer[] imageEnhancers); + string GetImageCacheTag(BaseItem item, ChapterInfo chapter); + + string? GetImageCacheTag(User user); /// <summary> /// Processes the image. @@ -95,36 +75,20 @@ namespace MediaBrowser.Controller.Drawing /// </summary> /// <param name="options">The options.</param> /// <returns>Task.</returns> - Task<(string path, string mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options); - - /// <summary> - /// Gets the enhanced image. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="imageType">Type of the image.</param> - /// <param name="imageIndex">Index of the image.</param> - /// <returns>Task{System.String}.</returns> - Task<string> GetEnhancedImage(BaseItem item, ImageType imageType, int imageIndex); + Task<(string path, string? mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options); /// <summary> /// Gets the supported image output formats. /// </summary> - /// <returns>IReadOnlyCollection{ImageOutput}.</returns> + /// <returns><see cref="IReadOnlyCollection{ImageOutput}" />.</returns> IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats(); /// <summary> /// Creates the image collage. /// </summary> /// <param name="options">The options.</param> - void CreateImageCollage(ImageCollageOptions options); - - /// <summary> - /// Gets a value indicating whether [supports image collage creation]. - /// </summary> - /// <value><c>true</c> if [supports image collage creation]; otherwise, <c>false</c>.</value> - bool SupportsImageCollageCreation { get; } - - IImageEncoder ImageEncoder { get; set; } + /// <param name="libraryName">The library name to draw onto the collage.</param> + void CreateImageCollage(ImageCollageOptions options, string? libraryName); bool SupportsTransparency(string path); } diff --git a/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs b/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs index 3f762fad0..e9c88ffb5 100644 --- a/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs +++ b/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs @@ -1,3 +1,9 @@ +#nullable disable + +using System.Collections.Generic; + +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Drawing { public class ImageCollageOptions @@ -6,17 +12,20 @@ namespace MediaBrowser.Controller.Drawing /// Gets or sets the input paths. /// </summary> /// <value>The input paths.</value> - public string[] InputPaths { get; set; } + public IReadOnlyList<string> InputPaths { get; set; } + /// <summary> /// Gets or sets the output path. /// </summary> /// <value>The output path.</value> public string OutputPath { get; set; } + /// <summary> /// Gets or sets the width. /// </summary> /// <value>The width.</value> public int Width { get; set; } + /// <summary> /// Gets or sets the height. /// </summary> diff --git a/MediaBrowser.Controller/Drawing/ImageHelper.cs b/MediaBrowser.Controller/Drawing/ImageHelper.cs index 432cf8042..9ef92bc98 100644 --- a/MediaBrowser.Controller/Drawing/ImageHelper.cs +++ b/MediaBrowser.Controller/Drawing/ImageHelper.cs @@ -1,73 +1,17 @@ -using System; -using MediaBrowser.Controller.Entities; +#pragma warning disable CS1591 + using MediaBrowser.Model.Drawing; -using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Drawing { public static class ImageHelper { - public static ImageDimensions GetNewImageSize(ImageProcessingOptions options, ImageDimensions? originalImageSize) - { - if (originalImageSize.HasValue) - { - // Determine the output size based on incoming parameters - var newSize = DrawingUtils.Resize(originalImageSize.Value, options.Width ?? 0, options.Height ?? 0, options.MaxWidth ?? 0, options.MaxHeight ?? 0); - - return newSize; - } - return GetSizeEstimate(options); - } - - public static IImageProcessor ImageProcessor { get; set; } - - private static ImageDimensions GetSizeEstimate(ImageProcessingOptions options) - { - if (options.Width.HasValue && options.Height.HasValue) - { - return new ImageDimensions(options.Width.Value, options.Height.Value); - } - - double aspect = GetEstimatedAspectRatio(options.Image.Type, options.Item); - - int? width = options.Width ?? options.MaxWidth; - - if (width.HasValue) - { - int heightValue = Convert.ToInt32((double)width.Value / aspect); - return new ImageDimensions(width.Value, heightValue); - } - - var height = options.Height ?? options.MaxHeight ?? 200; - int widthValue = Convert.ToInt32(aspect * height); - return new ImageDimensions(widthValue, height); - } - - private static double GetEstimatedAspectRatio(ImageType type, BaseItem item) + public static ImageDimensions GetNewImageSize(ImageProcessingOptions options, ImageDimensions originalImageSize) { - switch (type) - { - case ImageType.Art: - case ImageType.Backdrop: - case ImageType.Chapter: - case ImageType.Screenshot: - case ImageType.Thumb: - return 1.78; - case ImageType.Banner: - return 5.4; - case ImageType.Box: - case ImageType.BoxRear: - case ImageType.Disc: - case ImageType.Menu: - return 1; - case ImageType.Logo: - return 2.58; - case ImageType.Primary: - double defaultPrimaryImageAspectRatio = item.GetDefaultPrimaryImageAspectRatio(); - return defaultPrimaryImageAspectRatio > 0 ? defaultPrimaryImageAspectRatio : 2.0 / 3; - default: - return 1; - } + // Determine the output size based on incoming parameters + var newSize = DrawingUtils.Resize(originalImageSize, options.Width ?? 0, options.Height ?? 0, options.MaxWidth ?? 0, options.MaxHeight ?? 0); + newSize = DrawingUtils.ResizeFill(newSize, options.FillWidth, options.FillHeight); + return newSize; } } } diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs index db432f500..11e663301 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs @@ -1,8 +1,12 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; +using System.Collections.Generic; using System.IO; using System.Linq; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Drawing; namespace MediaBrowser.Controller.Drawing @@ -15,14 +19,13 @@ namespace MediaBrowser.Controller.Drawing } public Guid ItemId { get; set; } + public BaseItem Item { get; set; } public ItemImageInfo Image { get; set; } public int ImageIndex { get; set; } - public bool CropWhiteSpace { get; set; } - public int? Width { get; set; } public int? Height { get; set; } @@ -31,21 +34,26 @@ namespace MediaBrowser.Controller.Drawing public int? MaxHeight { get; set; } - public int Quality { get; set; } + public int? FillWidth { get; set; } - public IImageEnhancer[] Enhancers { get; set; } + public int? FillHeight { get; set; } - public ImageFormat[] SupportedOutputFormats { get; set; } + public int Quality { get; set; } + + public IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; set; } public bool AddPlayedIndicator { get; set; } public int? UnplayedCount { get; set; } + public int? Blur { get; set; } public double PercentPlayed { get; set; } public string BackgroundColor { get; set; } + public string ForegroundLayer { get; set; } + public bool RequiresAutoOrientation { get; set; } private bool HasDefaultOptions(string originalImagePath) @@ -75,19 +83,27 @@ namespace MediaBrowser.Controller.Drawing { return false; } + if (Height.HasValue && !sizeValue.Height.Equals(Height.Value)) { return false; } + if (MaxWidth.HasValue && sizeValue.Width > MaxWidth.Value) { return false; } + if (MaxHeight.HasValue && sizeValue.Height > MaxHeight.Value) { return false; } + if (sizeValue.Width > FillWidth || sizeValue.Height > FillHeight) + { + return false; + } + return true; } @@ -99,7 +115,6 @@ namespace MediaBrowser.Controller.Drawing PercentPlayed.Equals(0) && !UnplayedCount.HasValue && !Blur.HasValue && - !CropWhiteSpace && string.IsNullOrEmpty(BackgroundColor) && string.IsNullOrEmpty(ForegroundLayer); } diff --git a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs index df9050de5..b036425ab 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Controller/Drawing/ImageStream.cs b/MediaBrowser.Controller/Drawing/ImageStream.cs index 74ac24ec9..f4c305799 100644 --- a/MediaBrowser.Controller/Drawing/ImageStream.cs +++ b/MediaBrowser.Controller/Drawing/ImageStream.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA1711, CS1591 + using System; using System.IO; using MediaBrowser.Model.Drawing; @@ -6,11 +8,17 @@ namespace MediaBrowser.Controller.Drawing { public class ImageStream : IDisposable { + public ImageStream(Stream stream) + { + Stream = stream; + } + /// <summary> - /// Gets or sets the stream. + /// Gets the stream. /// </summary> /// <value>The stream.</value> - public Stream Stream { get; set; } + public Stream Stream { get; } + /// <summary> /// Gets or sets the format. /// </summary> @@ -19,9 +27,15 @@ namespace MediaBrowser.Controller.Drawing public void Dispose() { - if (Stream != null) + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) { - Stream.Dispose(); + Stream?.Dispose(); } } } diff --git a/MediaBrowser.Controller/Dto/DtoOptions.cs b/MediaBrowser.Controller/Dto/DtoOptions.cs index aa99f6b58..ecc833154 100644 --- a/MediaBrowser.Controller/Dto/DtoOptions.cs +++ b/MediaBrowser.Controller/Dto/DtoOptions.cs @@ -1,4 +1,9 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; +using System.Collections.Generic; using System.Linq; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; @@ -13,31 +18,15 @@ namespace MediaBrowser.Controller.Dto ItemFields.RefreshState }; - public ItemFields[] Fields { get; set; } - public ImageType[] ImageTypes { get; set; } - public int ImageTypeLimit { get; set; } - public bool EnableImages { get; set; } - public bool AddProgramRecordingInfo { get; set; } - public bool EnableUserData { get; set; } - public bool AddCurrentProgram { get; set; } - - public DtoOptions() - : this(true) - { - } - - private static readonly ImageType[] AllImageTypes = Enum.GetNames(typeof(ImageType)) - .Select(i => (ImageType)Enum.Parse(typeof(ImageType), i, true)) - .ToArray(); + private static readonly ImageType[] AllImageTypes = Enum.GetValues<ImageType>(); - private static readonly ItemFields[] AllItemFields = Enum.GetNames(typeof(ItemFields)) - .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)) + private static readonly ItemFields[] AllItemFields = Enum.GetValues<ItemFields>() .Except(DefaultExcludedFields) .ToArray(); - public bool ContainsField(ItemFields field) + public DtoOptions() + : this(true) { - return AllItemFields.Contains(field); } public DtoOptions(bool allFields) @@ -47,18 +36,27 @@ namespace MediaBrowser.Controller.Dto EnableUserData = true; AddCurrentProgram = true; - if (allFields) - { - Fields = AllItemFields; - } - else - { - Fields = new ItemFields[] { }; - } - + Fields = allFields ? AllItemFields : Array.Empty<ItemFields>(); ImageTypes = AllImageTypes; } + public IReadOnlyList<ItemFields> Fields { get; set; } + + public IReadOnlyList<ImageType> ImageTypes { get; set; } + + public int ImageTypeLimit { get; set; } + + public bool EnableImages { get; set; } + + public bool AddProgramRecordingInfo { get; set; } + + public bool EnableUserData { get; set; } + + public bool AddCurrentProgram { get; set; } + + public bool ContainsField(ItemFields field) + => Fields.Contains(field); + public int GetImageLimit(ImageType type) { if (EnableImages && ImageTypes.Contains(type)) diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs index df5ec5dd0..89aafc84f 100644 --- a/MediaBrowser.Controller/Dto/IDtoService.cs +++ b/MediaBrowser.Controller/Dto/IDtoService.cs @@ -1,30 +1,19 @@ +#nullable disable +#pragma warning disable CA1002 + using System.Collections.Generic; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Querying; namespace MediaBrowser.Controller.Dto { /// <summary> - /// Interface IDtoService + /// Interface IDtoService. /// </summary> public interface IDtoService { /// <summary> - /// Gets the dto id. - /// </summary> - /// <param name="item">The item.</param> - /// <returns>System.String.</returns> - string GetDtoId(BaseItem item); - - /// <summary> - /// Attaches the primary image aspect ratio. - /// </summary> - /// <param name="dto">The dto.</param> - /// <param name="item">The item.</param> - void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item); - - /// <summary> /// Gets the primary image aspect ratio. /// </summary> /// <param name="item">The item.</param> @@ -35,15 +24,6 @@ namespace MediaBrowser.Controller.Dto /// Gets the base item dto. /// </summary> /// <param name="item">The item.</param> - /// <param name="fields">The fields.</param> - /// <param name="user">The user.</param> - /// <param name="owner">The owner.</param> - BaseItemDto GetBaseItemDto(BaseItem item, ItemFields[] fields, User user = null, BaseItem owner = null); - - /// <summary> - /// Gets the base item dto. - /// </summary> - /// <param name="item">The item.</param> /// <param name="options">The options.</param> /// <param name="user">The user.</param> /// <param name="owner">The owner.</param> @@ -57,13 +37,17 @@ namespace MediaBrowser.Controller.Dto /// <param name="options">The options.</param> /// <param name="user">The user.</param> /// <param name="owner">The owner.</param> - BaseItemDto[] GetBaseItemDtos(BaseItem[] items, DtoOptions options, User user = null, BaseItem owner = null); - - BaseItemDto[] GetBaseItemDtos(List<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null); + /// <returns>The <see cref="IReadOnlyList{T}"/> of <see cref="BaseItemDto"/>.</returns> + IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null); /// <summary> /// Gets the item by name dto. /// </summary> + /// <param name="item">The item.</param> + /// <param name="options">The dto options.</param> + /// <param name="taggedItems">The list of tagged items.</param> + /// <param name="user">The user.</param> + /// <returns>The item dto.</returns> BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem> taggedItems, User user = null); } } diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs index 054df21e5..9589f5245 100644 --- a/MediaBrowser.Controller/Entities/AggregateFolder.cs +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -1,43 +1,40 @@ +#nullable disable + +#pragma warning disable CA1819, CS1591 + using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { /// <summary> /// Specialized folder that can have items added to it's children by external entities. - /// Used for our RootFolder so plug-ins can add items. + /// Used for our RootFolder so plugins can add items. /// </summary> public class AggregateFolder : Folder { - public AggregateFolder() - { - PhysicalLocationsList = Array.Empty<string>(); - } - - [IgnoreDataMember] - public override bool IsPhysicalRoot => true; - - public override bool CanDelete() - { - return false; - } - - [IgnoreDataMember] - public override bool SupportsPlayedStatus => false; + private readonly object _childIdsLock = new object(); /// <summary> - /// The _virtual children + /// The _virtual children. /// </summary> private readonly ConcurrentBag<BaseItem> _virtualChildren = new ConcurrentBag<BaseItem>(); + private bool _requiresRefresh; + private Guid[] _childrenIds = null; + + public AggregateFolder() + { + PhysicalLocationsList = Array.Empty<string>(); + } /// <summary> /// Gets the virtual children. @@ -45,18 +42,27 @@ namespace MediaBrowser.Controller.Entities /// <value>The virtual children.</value> public ConcurrentBag<BaseItem> VirtualChildren => _virtualChildren; - [IgnoreDataMember] + [JsonIgnore] + public override bool IsPhysicalRoot => true; + + [JsonIgnore] + public override bool SupportsPlayedStatus => false; + + [JsonIgnore] public override string[] PhysicalLocations => PhysicalLocationsList; public string[] PhysicalLocationsList { get; set; } + public override bool CanDelete() + { + return false; + } + protected override FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService) { return CreateResolveArgs(directoryService, true).FileSystemChildren; } - private Guid[] _childrenIds = null; - private readonly object _childIdsLock = new object(); protected override List<BaseItem> LoadChildren() { lock (_childIdsLock) @@ -80,7 +86,6 @@ namespace MediaBrowser.Controller.Entities } } - private bool _requiresRefresh; public override bool RequiresRefresh() { var changed = base.RequiresRefresh() || _requiresRefresh; @@ -89,7 +94,7 @@ namespace MediaBrowser.Controller.Entities { var locations = PhysicalLocations; - var newLocations = CreateResolveArgs(new DirectoryService(Logger, FileSystem), false).PhysicalLocations; + var newLocations = CreateResolveArgs(new DirectoryService(FileSystem), false).PhysicalLocations; if (!locations.SequenceEqual(newLocations)) { @@ -100,11 +105,11 @@ namespace MediaBrowser.Controller.Entities return changed; } - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { ClearCache(); - var changed = base.BeforeMetadataRefresh(replaceAllMetdata) || _requiresRefresh; + var changed = base.BeforeMetadataRefresh(replaceAllMetadata) || _requiresRefresh; _requiresRefresh = false; return changed; } @@ -117,8 +122,7 @@ namespace MediaBrowser.Controller.Entities var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService) { - FileInfo = FileSystem.GetDirectoryInfo(path), - Path = path + FileInfo = FileSystem.GetDirectoryInfo(path) }; // Gather child folder and files @@ -150,11 +154,11 @@ namespace MediaBrowser.Controller.Entities return base.GetNonCachedChildren(directoryService).Concat(_virtualChildren); } - protected override async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + protected override async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { ClearCache(); - await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService) + await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken) .ConfigureAwait(false); ClearCache(); @@ -164,7 +168,7 @@ namespace MediaBrowser.Controller.Entities /// Adds the virtual child. /// </summary> /// <param name="child">The child.</param> - /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentNullException">Throws if child is null.</exception> public void AddVirtualChild(BaseItem child) { if (child == null) @@ -180,7 +184,7 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <param name="id">The id.</param> /// <returns>BaseItem.</returns> - /// <exception cref="ArgumentNullException">id</exception> + /// <exception cref="ArgumentNullException">The id is empty.</exception> public BaseItem FindVirtualChild(Guid id) { if (id.Equals(Guid.Empty)) @@ -195,6 +199,7 @@ namespace MediaBrowser.Controller.Entities return child; } } + return null; } } diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index 13a6fe44a..536668e50 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -1,16 +1,22 @@ +#nullable disable + +#pragma warning disable CA1002, CA1724, CA1826, CS1591 + using System; using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities.Audio { /// <summary> - /// Class Audio + /// Class Audio. /// </summary> public class Audio : BaseItem, IHasAlbumArtist, @@ -19,105 +25,79 @@ namespace MediaBrowser.Controller.Entities.Audio IHasLookupInfo<SongInfo>, IHasMediaSources { - /// <summary> - /// Gets or sets the artist. - /// </summary> - /// <value>The artist.</value> - [IgnoreDataMember] - public string[] Artists { get; set; } - - [IgnoreDataMember] - public string[] AlbumArtists { get; set; } - public Audio() { Artists = Array.Empty<string>(); AlbumArtists = Array.Empty<string>(); } - public override double GetDefaultPrimaryImageAspectRatio() - { - return 1; - } + /// <inheritdoc /> + [JsonIgnore] + public IReadOnlyList<string> Artists { get; set; } + + /// <inheritdoc /> + [JsonIgnore] + public IReadOnlyList<string> AlbumArtists { get; set; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => true; - [IgnoreDataMember] - public override bool SupportsPeople => false; + [JsonIgnore] + public override bool SupportsPeople => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsAddingToPlaylist => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => true; - [IgnoreDataMember] + [JsonIgnore] protected override bool SupportsOwnedItems => false; - [IgnoreDataMember] + [JsonIgnore] public override Folder LatestItemsIndexContainer => AlbumEntity; - public override bool CanDownload() - { - return IsFileProtocol; - } - - [IgnoreDataMember] - public string[] AllArtists - { - get - { - var list = new string[AlbumArtists.Length + Artists.Length]; - - var index = 0; - foreach (var artist in AlbumArtists) - { - list[index] = artist; - index++; - } - foreach (var artist in Artists) - { - list[index] = artist; - index++; - } - - return list; - - } - } - - [IgnoreDataMember] + [JsonIgnore] public MusicAlbum AlbumEntity => FindParent<MusicAlbum>(); /// <summary> /// Gets the type of the media. /// </summary> /// <value>The type of the media.</value> - [IgnoreDataMember] + [JsonIgnore] public override string MediaType => Model.Entities.MediaType.Audio; + public override double GetDefaultPrimaryImageAspectRatio() + { + return 1; + } + + public override bool CanDownload() + { + return IsFileProtocol; + } + /// <summary> /// Creates the name of the sort. /// </summary> /// <returns>System.String.</returns> protected override string CreateSortName() { - return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("0000 - ") : "") - + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ") : "") + Name; + return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) + + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) + Name; } public override List<string> GetUserDataKeys() { var list = base.GetUserDataKeys(); - var songKey = IndexNumber.HasValue ? IndexNumber.Value.ToString("0000") : string.Empty; - + var songKey = IndexNumber.HasValue ? IndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) : string.Empty; if (ParentIndexNumber.HasValue) { - songKey = ParentIndexNumber.Value.ToString("0000") + "-" + songKey; + songKey = ParentIndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) + "-" + songKey; } + songKey += Name; if (!string.IsNullOrEmpty(Album)) @@ -125,7 +105,7 @@ namespace MediaBrowser.Controller.Entities.Audio songKey = Album + "-" + songKey; } - var albumArtist = AlbumArtists.Length == 0 ? null : AlbumArtists[0]; + var albumArtist = AlbumArtists.FirstOrDefault(); if (!string.IsNullOrEmpty(albumArtist)) { songKey = albumArtist + "-" + songKey; @@ -142,6 +122,7 @@ namespace MediaBrowser.Controller.Entities.Audio { return UnratedItem.Music; } + return base.GetBlockUnratedType(); } diff --git a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs index a269b3486..1625c748a 100644 --- a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs @@ -1,14 +1,33 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Controller.Library; + namespace MediaBrowser.Controller.Entities.Audio { public interface IHasAlbumArtist { - string[] AlbumArtists { get; set; } + IReadOnlyList<string> AlbumArtists { get; set; } } public interface IHasArtist { - string[] AllArtists { get; } + /// <summary> + /// Gets or sets the artists. + /// </summary> + /// <value>The artists.</value> + IReadOnlyList<string> Artists { get; set; } + } - string[] Artists { get; set; } + public static class Extentions + { + public static IEnumerable<string> GetAllArtists<T>(this T item) + where T : IHasArtist, IHasAlbumArtist + { + return item.AlbumArtists.Concat(item.Artists).DistinctNames(); + } } } diff --git a/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs b/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs index 5ae056050..c2dae5a2d 100644 --- a/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs +++ b/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CA1819, CS1591 + namespace MediaBrowser.Controller.Entities.Audio { public interface IHasMusicGenres diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index 5b2939b75..03d1f3304 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -1,48 +1,74 @@ +#nullable disable + +#pragma warning disable CA1721, CA1826, CS1591 + using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; +using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider; namespace MediaBrowser.Controller.Entities.Audio { /// <summary> - /// Class MusicAlbum + /// Class MusicAlbum. /// </summary> public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo<AlbumInfo>, IMetadataContainer { - public string[] AlbumArtists { get; set; } - public string[] Artists { get; set; } - public MusicAlbum() { Artists = Array.Empty<string>(); AlbumArtists = Array.Empty<string>(); } - [IgnoreDataMember] + /// <inheritdoc /> + public IReadOnlyList<string> AlbumArtists { get; set; } + + /// <inheritdoc /> + public IReadOnlyList<string> Artists { get; set; } + + [JsonIgnore] public override bool SupportsAddingToPlaylist => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => true; - [IgnoreDataMember] + [JsonIgnore] public MusicArtist MusicArtist => GetMusicArtist(new DtoOptions(true)); + [JsonIgnore] + public override bool SupportsPlayedStatus => false; + + [JsonIgnore] + public override bool SupportsCumulativeRunTimeTicks => true; + + [JsonIgnore] + public string AlbumArtist => AlbumArtists.FirstOrDefault(); + + [JsonIgnore] + public override bool SupportsPeople => false; + + /// <summary> + /// Gets the tracks. + /// </summary> + /// <value>The tracks.</value> + [JsonIgnore] + public IEnumerable<Audio> Tracks => GetRecursiveChildren(i => i is Audio).Cast<Audio>(); + public MusicArtist GetMusicArtist(DtoOptions options) { var parents = GetParents(); foreach (var parent in parents) { - var artist = parent as MusicArtist; - if (artist != null) + if (parent is MusicArtist artist) { return artist; } @@ -53,51 +79,10 @@ namespace MediaBrowser.Controller.Entities.Audio { return LibraryManager.GetArtist(name, options); } - return null; - } - - [IgnoreDataMember] - public override bool SupportsPlayedStatus => false; - [IgnoreDataMember] - public override bool SupportsCumulativeRunTimeTicks => true; - - [IgnoreDataMember] - public string[] AllArtists - { - get - { - var list = new string[AlbumArtists.Length + Artists.Length]; - - var index = 0; - foreach (var artist in AlbumArtists) - { - list[index] = artist; - index++; - } - foreach (var artist in Artists) - { - list[index] = artist; - index++; - } - - return list; - } + return null; } - [IgnoreDataMember] - public string AlbumArtist => AlbumArtists.Length == 0 ? null : AlbumArtists[0]; - - [IgnoreDataMember] - public override bool SupportsPeople => false; - - /// <summary> - /// Gets the tracks. - /// </summary> - /// <value>The tracks.</value> - [IgnoreDataMember] - public IEnumerable<Audio> Tracks => GetRecursiveChildren(i => i is Audio).Cast<Audio>(); - protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) { return Tracks; @@ -118,14 +103,14 @@ namespace MediaBrowser.Controller.Entities.Audio list.Insert(0, albumArtist + "-" + Name); } - var id = this.GetProviderId(MetadataProviders.MusicBrainzAlbum); + var id = this.GetProviderId(MetadataProvider.MusicBrainzAlbum); if (!string.IsNullOrEmpty(id)) { list.Insert(0, "MusicAlbum-Musicbrainz-" + id); } - id = this.GetProviderId(MetadataProviders.MusicBrainzReleaseGroup); + id = this.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup); if (!string.IsNullOrEmpty(id)) { @@ -135,9 +120,9 @@ namespace MediaBrowser.Controller.Entities.Audio return list; } - protected override bool GetBlockUnratedValue(UserPolicy config) + protected override bool GetBlockUnratedValue(User user) { - return config.BlockUnratedItems.Contains(UnratedItem.Music); + return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music); } public override UnratedItem GetBlockUnratedType() @@ -216,8 +201,7 @@ namespace MediaBrowser.Controller.Entities.Audio private async Task RefreshArtists(MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) { - var all = AllArtists; - foreach (var i in all) + foreach (var i in this.GetAllArtists()) { // This should not be necessary but we're seeing some cases of it if (string.IsNullOrEmpty(i)) diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 2d464bd32..f30f8ce7f 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -1,44 +1,79 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Controller.Extensions; +using Diacritics.Extensions; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; using Microsoft.Extensions.Logging; +using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider; namespace MediaBrowser.Controller.Entities.Audio { /// <summary> - /// Class MusicArtist + /// Class MusicArtist. /// </summary> public class MusicArtist : Folder, IItemByName, IHasMusicGenres, IHasDualAccess, IHasLookupInfo<ArtistInfo> { - [IgnoreDataMember] + [JsonIgnore] public bool IsAccessedByName => ParentId.Equals(Guid.Empty); - [IgnoreDataMember] + [JsonIgnore] public override bool IsFolder => !IsAccessedByName; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsCumulativeRunTimeTicks => true; - [IgnoreDataMember] + [JsonIgnore] public override bool IsDisplayedAsFolder => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsAddingToPlaylist => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => false; + /// <summary> + /// Gets the folder containing the item. + /// If the item is a folder, it returns the folder itself. + /// </summary> + /// <value>The containing folder path.</value> + [JsonIgnore] + public override string ContainingFolderPath => Path; + + [JsonIgnore] + public override IEnumerable<BaseItem> Children + { + get + { + if (IsAccessedByName) + { + return new List<BaseItem>(); + } + + return base.Children; + } + } + + [JsonIgnore] + public override bool SupportsPeople => false; + + public static string GetPath(string name) + { + return GetPath(name, true); + } + public override double GetDefaultPrimaryImageAspectRatio() { return 1; @@ -53,34 +88,16 @@ namespace MediaBrowser.Controller.Entities.Audio { if (query.IncludeItemTypes.Length == 0) { - query.IncludeItemTypes = new[] { typeof(Audio).Name, typeof(MusicVideo).Name, typeof(MusicAlbum).Name }; + query.IncludeItemTypes = new[] { nameof(Audio), nameof(MusicVideo), nameof(MusicAlbum) }; query.ArtistIds = new[] { Id }; } return LibraryManager.GetItemList(query); } - [IgnoreDataMember] - public override IEnumerable<BaseItem> Children - { - get - { - if (IsAccessedByName) - { - return new List<BaseItem>(); - } - - return base.Children; - } - } - public override int GetChildCount(User user) { - if (IsAccessedByName) - { - return 0; - } - return base.GetChildCount(user); + return IsAccessedByName ? 0 : base.GetChildCount(user); } public override bool IsSaveLocalMetadataEnabled() @@ -93,7 +110,7 @@ namespace MediaBrowser.Controller.Entities.Audio return base.IsSaveLocalMetadataEnabled(); } - protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { if (IsAccessedByName) { @@ -101,7 +118,7 @@ namespace MediaBrowser.Controller.Entities.Audio return Task.CompletedTask; } - return base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService); + return base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken); } public override List<string> GetUserDataKeys() @@ -113,14 +130,6 @@ namespace MediaBrowser.Controller.Entities.Audio } /// <summary> - /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself - /// </summary> - /// <value>The containing folder path.</value> - [IgnoreDataMember] - public override string ContainingFolderPath => Path; - - /// <summary> /// Gets the user data key. /// </summary> /// <param name="item">The item.</param> @@ -128,7 +137,7 @@ namespace MediaBrowser.Controller.Entities.Audio private static List<string> GetUserDataKeys(MusicArtist item) { var list = new List<string>(); - var id = item.GetProviderId(MetadataProviders.MusicBrainzArtist); + var id = item.GetProviderId(MetadataProvider.MusicBrainzArtist); if (!string.IsNullOrEmpty(id)) { @@ -138,13 +147,15 @@ namespace MediaBrowser.Controller.Entities.Audio list.Add("Artist-" + (item.Name ?? string.Empty).RemoveDiacritics()); return list; } + public override string CreatePresentationUniqueKey() { return "Artist-" + (Name ?? string.Empty).RemoveDiacritics(); } - protected override bool GetBlockUnratedValue(UserPolicy config) + + protected override bool GetBlockUnratedValue(User user) { - return config.BlockUnratedItems.Contains(UnratedItem.Music); + return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music); } public override UnratedItem GetBlockUnratedType() @@ -164,14 +175,6 @@ namespace MediaBrowser.Controller.Entities.Audio return info; } - [IgnoreDataMember] - public override bool SupportsPeople => false; - - public static string GetPath(string name) - { - return GetPath(name, true); - } - public static string GetPath(string name, bool normalizeName) { // Trim the period at the end because windows will have a hard time with that @@ -198,15 +201,18 @@ namespace MediaBrowser.Controller.Entities.Audio return true; } } + return base.RequiresRefresh(); } /// <summary> - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true or false indicating if changes were made. /// </summary> - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + /// <param name="replaceAllMetadata">Option to replace metadata.</param> + /// <returns>True if metadata changed.</returns> + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); if (IsAccessedByName) { diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs index d26aaf2bb..dc6fcc55a 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs @@ -1,16 +1,40 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; -using MediaBrowser.Controller.Extensions; -using MediaBrowser.Model.Serialization; +using System.Text.Json.Serialization; +using Diacritics.Extensions; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities.Audio { /// <summary> - /// Class MusicGenre + /// Class MusicGenre. /// </summary> public class MusicGenre : BaseItem, IItemByName { + [JsonIgnore] + public override bool SupportsAddingToPlaylist => true; + + [JsonIgnore] + public override bool SupportsAncestors => false; + + [JsonIgnore] + public override bool IsDisplayedAsFolder => true; + + /// <summary> + /// Gets the folder containing the item. + /// If the item is a folder, it returns the folder itself. + /// </summary> + /// <value>The containing folder path.</value> + [JsonIgnore] + public override string ContainingFolderPath => Path; + + [JsonIgnore] + public override bool SupportsPeople => false; + public override List<string> GetUserDataKeys() { var list = base.GetUserDataKeys(); @@ -18,28 +42,12 @@ namespace MediaBrowser.Controller.Entities.Audio list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics()); return list; } + public override string CreatePresentationUniqueKey() { return GetUserDataKeys()[0]; } - [IgnoreDataMember] - public override bool SupportsAddingToPlaylist => true; - - [IgnoreDataMember] - public override bool SupportsAncestors => false; - - [IgnoreDataMember] - public override bool IsDisplayedAsFolder => true; - - /// <summary> - /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself - /// </summary> - /// <value>The containing folder path.</value> - [IgnoreDataMember] - public override string ContainingFolderPath => Path; - public override double GetDefaultPrimaryImageAspectRatio() { return 1; @@ -55,13 +63,10 @@ namespace MediaBrowser.Controller.Entities.Audio return true; } - [IgnoreDataMember] - public override bool SupportsPeople => false; - public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) { query.GenreIds = new[] { Id }; - query.IncludeItemTypes = new[] { typeof(MusicVideo).Name, typeof(Audio).Name, typeof(MusicAlbum).Name, typeof(MusicArtist).Name }; + query.IncludeItemTypes = new[] { nameof(MusicVideo), nameof(Audio), nameof(MusicAlbum), nameof(MusicArtist) }; return LibraryManager.GetItemList(query); } @@ -94,15 +99,18 @@ namespace MediaBrowser.Controller.Entities.Audio Logger.LogDebug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); return true; } + return base.RequiresRefresh(); } /// <summary> - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true or false indicating if changes were made. /// </summary> - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + /// <param name="replaceAllMetadata">Option to replace metadata.</param> + /// <returns>True if metadata changed.</returns> + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); var newPath = GetRebasedPath(); if (!string.Equals(Path, newPath, StringComparison.Ordinal)) diff --git a/MediaBrowser.Controller/Entities/AudioBook.cs b/MediaBrowser.Controller/Entities/AudioBook.cs index 65c8a5fdd..782481fbc 100644 --- a/MediaBrowser.Controller/Entities/AudioBook.cs +++ b/MediaBrowser.Controller/Entities/AudioBook.cs @@ -1,33 +1,41 @@ +#nullable disable + +#pragma warning disable CA1724, CS1591 + using System; +using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { public class AudioBook : Audio.Audio, IHasSeries, IHasLookupInfo<SongInfo> { - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPositionTicksResume => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => true; - [IgnoreDataMember] + [JsonIgnore] public string SeriesPresentationUniqueKey { get; set; } - [IgnoreDataMember] + + [JsonIgnore] public string SeriesName { get; set; } - [IgnoreDataMember] + + [JsonIgnore] public Guid SeriesId { get; set; } public string FindSeriesSortName() { return SeriesName; } + public string FindSeriesName() { return SeriesName; } + public string FindSeriesPresentationUniqueKey() { return SeriesPresentationUniqueKey; diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 43fee79a1..838a9f2f8 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1,141 +1,223 @@ +#nullable disable + +#pragma warning disable CS1591, SA1401 + using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Diacritics.Extensions; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Extensions; +using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; -using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Library; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities { /// <summary> - /// Class BaseItem + /// Class BaseItem. /// </summary> - public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo> + public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo>, IEquatable<BaseItem> { - protected static MetadataFields[] EmptyMetadataFieldsArray = Array.Empty<MetadataFields>(); - protected static MediaUrl[] EmptyMediaUrlArray = Array.Empty<MediaUrl>(); - protected static ItemImageInfo[] EmptyItemImageInfoArray = Array.Empty<ItemImageInfo>(); - public static readonly LinkedChild[] EmptyLinkedChildArray = Array.Empty<LinkedChild>(); + /// <summary> + /// The trailer folder name. + /// </summary> + public const string TrailersFolderName = "trailers"; + public const string ThemeSongsFolderName = "theme-music"; + public const string ThemeSongFileName = "theme"; + public const string ThemeVideosFolderName = "backdrops"; + + /// <summary> + /// The supported image extensions. + /// </summary> + public static readonly string[] SupportedImageExtensions + = new[] { ".png", ".jpg", ".jpeg", ".tbn", ".gif" }; + + private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions) + { + ".nfo", + ".xml", + ".srt", + ".vtt", + ".sub", + ".idx", + ".txt", + ".edl", + ".bif", + ".smi", + ".ttml" + }; + + /// <summary> + /// Extra types that should be counted and displayed as "Special Features" in the UI. + /// </summary> + public static readonly IReadOnlyCollection<ExtraType> DisplayExtraTypes = new HashSet<ExtraType> + { + Model.Entities.ExtraType.Unknown, + Model.Entities.ExtraType.BehindTheScenes, + Model.Entities.ExtraType.Clip, + Model.Entities.ExtraType.DeletedScene, + Model.Entities.ExtraType.Interview, + Model.Entities.ExtraType.Sample, + Model.Entities.ExtraType.Scene + }; + + public static readonly char[] SlugReplaceChars = { '?', '/', '&' }; + + /// <summary> + /// The supported extra folder names and types. See <see cref="Emby.Naming.Common.NamingOptions" />. + /// </summary> + public static readonly Dictionary<string, ExtraType> AllExtrasTypesFolderNames = new Dictionary<string, ExtraType>(StringComparer.OrdinalIgnoreCase) + { + ["extras"] = MediaBrowser.Model.Entities.ExtraType.Unknown, + ["behind the scenes"] = MediaBrowser.Model.Entities.ExtraType.BehindTheScenes, + ["deleted scenes"] = MediaBrowser.Model.Entities.ExtraType.DeletedScene, + ["interviews"] = MediaBrowser.Model.Entities.ExtraType.Interview, + ["scenes"] = MediaBrowser.Model.Entities.ExtraType.Scene, + ["samples"] = MediaBrowser.Model.Entities.ExtraType.Sample, + ["shorts"] = MediaBrowser.Model.Entities.ExtraType.Clip, + ["featurettes"] = MediaBrowser.Model.Entities.ExtraType.Clip + }; + + private string _sortName; + private Guid[] _themeSongIds; + private Guid[] _themeVideoIds; + + private string _forcedSortName; + + private string _name; + + public const char SlugChar = '-'; protected BaseItem() { - ThemeSongIds = Array.Empty<Guid>(); - ThemeVideoIds = Array.Empty<Guid>(); Tags = Array.Empty<string>(); Genres = Array.Empty<string>(); Studios = Array.Empty<string>(); ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - LockedFields = EmptyMetadataFieldsArray; - ImageInfos = EmptyItemImageInfoArray; + LockedFields = Array.Empty<MetadataField>(); + ImageInfos = Array.Empty<ItemImageInfo>(); ProductionLocations = Array.Empty<string>(); RemoteTrailers = Array.Empty<MediaUrl>(); ExtraIds = Array.Empty<Guid>(); } - public static readonly char[] SlugReplaceChars = { '?', '/', '&' }; - public static char SlugChar = '-'; + [JsonIgnore] + public Guid[] ThemeSongIds + { + get + { + return _themeSongIds ??= GetExtras() + .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeSong) + .Select(song => song.Id) + .ToArray(); + } - /// <summary> - /// The supported image extensions - /// </summary> - public static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg", ".tbn", ".gif" }; + private set + { + _themeSongIds = value; + } + } - /// <summary> - /// The trailer folder name - /// </summary> - public static string TrailerFolderName = "trailers"; - public static string ThemeSongsFolderName = "theme-music"; - public static string ThemeSongFilename = "theme"; - public static string ThemeVideosFolderName = "backdrops"; + [JsonIgnore] + public Guid[] ThemeVideoIds + { + get + { + return _themeVideoIds ??= GetExtras() + .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeVideo) + .Select(song => song.Id) + .ToArray(); + } - [IgnoreDataMember] - public Guid[] ThemeSongIds { get; set; } - [IgnoreDataMember] - public Guid[] ThemeVideoIds { get; set; } + private set + { + _themeVideoIds = value; + } + } - [IgnoreDataMember] + [JsonIgnore] public string PreferredMetadataCountryCode { get; set; } - [IgnoreDataMember] + + [JsonIgnore] public string PreferredMetadataLanguage { get; set; } public long? Size { get; set; } + public string Container { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string Tagline { get; set; } - [IgnoreDataMember] + [JsonIgnore] public virtual ItemImageInfo[] ImageInfos { get; set; } - [IgnoreDataMember] + [JsonIgnore] public bool IsVirtualItem { get; set; } /// <summary> /// Gets or sets the album. /// </summary> /// <value>The album.</value> - [IgnoreDataMember] + [JsonIgnore] public string Album { get; set; } /// <summary> /// Gets or sets the channel identifier. /// </summary> /// <value>The channel identifier.</value> - [IgnoreDataMember] + [JsonIgnore] public Guid ChannelId { get; set; } - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsAddingToPlaylist => false; - [IgnoreDataMember] + [JsonIgnore] public virtual bool AlwaysScanInternalMetadataPath => false; /// <summary> - /// Gets a value indicating whether this instance is in mixed folder. + /// Gets or sets a value indicating whether this instance is in mixed folder. /// </summary> /// <value><c>true</c> if this instance is in mixed folder; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsInMixedFolder { get; set; } - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsPlayedStatus => false; - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsPositionTicksResume => false; - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsRemoteImageDownloading => true; - private string _name; /// <summary> /// Gets or sets the name. /// </summary> /// <value>The name.</value> - [IgnoreDataMember] + [JsonIgnore] public virtual string Name { get => _name; @@ -148,43 +230,44 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] public bool IsUnaired => PremiereDate.HasValue && PremiereDate.Value.ToLocalTime().Date >= DateTime.Now.Date; - [IgnoreDataMember] + [JsonIgnore] public int? TotalBitrate { get; set; } - [IgnoreDataMember] + + [JsonIgnore] public ExtraType? ExtraType { get; set; } - [IgnoreDataMember] + [JsonIgnore] public bool IsThemeMedia => ExtraType.HasValue && (ExtraType.Value == Model.Entities.ExtraType.ThemeSong || ExtraType.Value == Model.Entities.ExtraType.ThemeVideo); - [IgnoreDataMember] + [JsonIgnore] public string OriginalTitle { get; set; } /// <summary> /// Gets or sets the id. /// </summary> /// <value>The id.</value> - [IgnoreDataMember] + [JsonIgnore] public Guid Id { get; set; } - [IgnoreDataMember] + [JsonIgnore] public Guid OwnerId { get; set; } /// <summary> /// Gets or sets the audio. /// </summary> /// <value>The audio.</value> - [IgnoreDataMember] + [JsonIgnore] public ProgramAudio? Audio { get; set; } /// <summary> - /// Return the id that should be used to key display prefs for this item. + /// Gets the id that should be used to key display prefs for this item. /// Default is based on the type for everything except actual generic folders. /// </summary> /// <value>The display prefs id.</value> - [IgnoreDataMember] + [JsonIgnore] public virtual Guid DisplayPreferencesId { get @@ -198,10 +281,10 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the path. /// </summary> /// <value>The path.</value> - [IgnoreDataMember] + [JsonIgnore] public virtual string Path { get; set; } - [IgnoreDataMember] + [JsonIgnore] public virtual SourceType SourceType { get @@ -216,10 +299,10 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself + /// Gets the folder containing the item. + /// If the item is a folder, it returns the folder itself. /// </summary> - [IgnoreDataMember] + [JsonIgnore] public virtual string ContainingFolderPath { get @@ -237,47 +320,44 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the name of the service. /// </summary> /// <value>The name of the service.</value> - [IgnoreDataMember] + [JsonIgnore] public string ServiceName { get; set; } /// <summary> - /// If this content came from an external service, the id of the content on that service + /// Gets or sets the external id. /// </summary> - [IgnoreDataMember] + /// <remarks> + /// If this content came from an external service, the id of the content on that service. + /// </remarks> + [JsonIgnore] public string ExternalId { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string ExternalSeriesId { get; set; } /// <summary> /// Gets or sets the etag. /// </summary> /// <value>The etag.</value> - [IgnoreDataMember] + [JsonIgnore] public string ExternalEtag { get; set; } - [IgnoreDataMember] + [JsonIgnore] public virtual bool IsHidden => false; - public BaseItem GetOwner() - { - var ownerId = OwnerId; - return ownerId.Equals(Guid.Empty) ? null : LibraryManager.GetItemById(ownerId); - } - /// <summary> - /// Gets or sets the type of the location. + /// Gets the type of the location. /// </summary> /// <value>The type of the location.</value> - [IgnoreDataMember] + [JsonIgnore] public virtual LocationType LocationType { get { - //if (IsOffline) - //{ + // if (IsOffline) + // { // return LocationType.Offline; - //} + // } var path = Path; if (string.IsNullOrEmpty(path)) @@ -294,7 +374,7 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] public MediaProtocol? PathProtocol { get @@ -310,20 +390,13 @@ namespace MediaBrowser.Controller.Entities } } - public bool IsPathProtocol(MediaProtocol protocol) - { - var current = PathProtocol; - - return current.HasValue && current.Value == protocol; - } - - [IgnoreDataMember] + [JsonIgnore] public bool IsFileProtocol => IsPathProtocol(MediaProtocol.File); - [IgnoreDataMember] + [JsonIgnore] public bool HasPathProtocol => PathProtocol.HasValue; - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsLocalMetadata { get @@ -337,7 +410,7 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] public virtual string FileNameWithoutExtension { get @@ -351,452 +424,165 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] public virtual bool EnableAlphaNumericSorting => true; - private List<Tuple<StringBuilder, bool>> GetSortChunks(string s1) - { - var list = new List<Tuple<StringBuilder, bool>>(); - - int thisMarker = 0; - - while (thisMarker < s1.Length) - { - if (thisMarker >= s1.Length) - { - break; - } - char thisCh = s1[thisMarker]; + public virtual bool IsHD => Height >= 720; - var thisChunk = new StringBuilder(); + public bool IsShortcut { get; set; } - while ((thisMarker < s1.Length) && (thisChunk.Length == 0 || SortHelper.InChunk(thisCh, thisChunk[0]))) - { - thisChunk.Append(thisCh); - thisMarker++; + public string ShortcutPath { get; set; } - if (thisMarker < s1.Length) - { - thisCh = s1[thisMarker]; - } - } + public int Width { get; set; } - var isNumeric = thisChunk.Length > 0 && char.IsDigit(thisChunk[0]); - list.Add(new Tuple<StringBuilder, bool>(thisChunk, isNumeric)); - } + public int Height { get; set; } - return list; - } + public Guid[] ExtraIds { get; set; } /// <summary> - /// This is just a helper for convenience + /// Gets the primary image path. /// </summary> + /// <remarks> + /// This is just a helper for convenience. + /// </remarks> /// <value>The primary image path.</value> - [IgnoreDataMember] + [JsonIgnore] public string PrimaryImagePath => this.GetImagePath(ImageType.Primary); - public bool IsMetadataFetcherEnabled(LibraryOptions libraryOptions, string name) - { - if (SourceType == SourceType.Channel) - { - // hack alert - return !EnableMediaSourceDisplay; - } - - var typeOptions = libraryOptions.GetTypeOptions(GetType().Name); - if (typeOptions != null) - { - return typeOptions.MetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); - } - - if (!libraryOptions.EnableInternetProviders) - { - return false; - } - - var itemConfig = ConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase)); - - return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); - } - - public bool IsImageFetcherEnabled(LibraryOptions libraryOptions, string name) - { - if (this is Channel) - { - // hack alert - return true; - } - if (SourceType == SourceType.Channel) - { - // hack alert - return !EnableMediaSourceDisplay; - } - - var typeOptions = libraryOptions.GetTypeOptions(GetType().Name); - if (typeOptions != null) - { - return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); - } - - if (!libraryOptions.EnableInternetProviders) - { - return false; - } - - var itemConfig = ConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase)); - - return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); - } - - public virtual bool CanDelete() - { - if (SourceType == SourceType.Channel) - { - return ChannelManager.CanDelete(this); - } - - return IsFileProtocol; - } - - public virtual bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders) - { - if (user.Policy.EnableContentDeletion) - { - return true; - } - - var allowed = user.Policy.EnableContentDeletionFromFolders; - - if (SourceType == SourceType.Channel) - { - return allowed.Contains(ChannelId.ToString(""), StringComparer.OrdinalIgnoreCase); - } - else - { - var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders); - - foreach (var folder in collectionFolders) - { - if (allowed.Contains(folder.Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) - { - return true; - } - } - } - - return false; - } - - public bool CanDelete(User user, List<Folder> allCollectionFolders) - { - return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders); - } - - public bool CanDelete(User user) - { - var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList(); - - return CanDelete(user, allCollectionFolders); - } - - public virtual bool CanDownload() - { - return false; - } - - public virtual bool IsAuthorizedToDownload(User user) - { - return user.Policy.EnableContentDownloading; - } - - public bool CanDownload(User user) - { - return CanDownload() && IsAuthorizedToDownload(user); - } - /// <summary> /// Gets or sets the date created. /// </summary> /// <value>The date created.</value> - [IgnoreDataMember] + [JsonIgnore] public DateTime DateCreated { get; set; } /// <summary> /// Gets or sets the date modified. /// </summary> /// <value>The date modified.</value> - [IgnoreDataMember] + [JsonIgnore] public DateTime DateModified { get; set; } - [IgnoreDataMember] public DateTime DateLastSaved { get; set; } - [IgnoreDataMember] + [JsonIgnore] public DateTime DateLastRefreshed { get; set; } - /// <summary> - /// The logger - /// </summary> - public static ILogger Logger { get; set; } - public static ILibraryManager LibraryManager { get; set; } - public static IServerConfigurationManager ConfigurationManager { get; set; } - public static IProviderManager ProviderManager { get; set; } - public static ILocalizationManager LocalizationManager { get; set; } - public static IItemRepository ItemRepository { get; set; } - public static IFileSystem FileSystem { get; set; } - public static IUserDataManager UserDataManager { get; set; } - public static IChannelManager ChannelManager { get; set; } - public static IMediaSourceManager MediaSourceManager { get; set; } - - /// <summary> - /// Returns a <see cref="string" /> that represents this instance. - /// </summary> - /// <returns>A <see cref="string" /> that represents this instance.</returns> - public override string ToString() - { - return Name; - } - - [IgnoreDataMember] + [JsonIgnore] public bool IsLocked { get; set; } /// <summary> /// Gets or sets the locked fields. /// </summary> /// <value>The locked fields.</value> - [IgnoreDataMember] - public MetadataFields[] LockedFields { get; set; } + [JsonIgnore] + public MetadataField[] LockedFields { get; set; } /// <summary> /// Gets the type of the media. /// </summary> /// <value>The type of the media.</value> - [IgnoreDataMember] + [JsonIgnore] public virtual string MediaType => null; - [IgnoreDataMember] + [JsonIgnore] public virtual string[] PhysicalLocations { get { if (!IsFileProtocol) { - return new string[] { }; + return Array.Empty<string>(); } return new[] { Path }; } } - private string _forcedSortName; - /// <summary> - /// Gets or sets the name of the forced sort. - /// </summary> - /// <value>The name of the forced sort.</value> - [IgnoreDataMember] - public string ForcedSortName - { - get => _forcedSortName; - set { _forcedSortName = value; _sortName = null; } - } - - private string _sortName; - /// <summary> - /// Gets the name of the sort. - /// </summary> - /// <value>The name of the sort.</value> - [IgnoreDataMember] - public string SortName + [JsonIgnore] + public bool EnableMediaSourceDisplay { get { - if (_sortName == null) + if (SourceType == SourceType.Channel) { - if (!string.IsNullOrEmpty(ForcedSortName)) - { - // Need the ToLower because that's what CreateSortName does - _sortName = ModifySortChunks(ForcedSortName).ToLowerInvariant(); - } - else - { - _sortName = CreateSortName(); - } + return ChannelManager.EnableMediaSourceDisplay(this); } - return _sortName; - } - set => _sortName = value; - } - - public string GetInternalMetadataPath() - { - var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath; - return GetInternalMetadataPath(basePath); - } - - protected virtual string GetInternalMetadataPath(string basePath) - { - if (SourceType == SourceType.Channel) - { - return System.IO.Path.Combine(basePath, "channels", ChannelId.ToString("N"), Id.ToString("N")); + return true; } - - var idString = Id.ToString("N"); - - basePath = System.IO.Path.Combine(basePath, "library"); - - return System.IO.Path.Combine(basePath, idString.Substring(0, 2), idString); } + [JsonIgnore] + public Guid ParentId { get; set; } + /// <summary> - /// Creates the name of the sort. + /// Gets or sets the logger. /// </summary> - /// <returns>System.String.</returns> - protected virtual string CreateSortName() - { - if (Name == null) return null; //some items may not have name filled in properly - - if (!EnableAlphaNumericSorting) - { - return Name.TrimStart(); - } - - var sortable = Name.Trim().ToLowerInvariant(); + public static ILogger<BaseItem> Logger { get; set; } - foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters) - { - sortable = sortable.Replace(removeChar, string.Empty); - } - - foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters) - { - sortable = sortable.Replace(replaceChar, " "); - } - - foreach (var search in ConfigurationManager.Configuration.SortRemoveWords) - { - // Remove from beginning if a space follows - if (sortable.StartsWith(search + " ")) - { - sortable = sortable.Remove(0, search.Length + 1); - } - // Remove from middle if surrounded by spaces - sortable = sortable.Replace(" " + search + " ", " "); - - // Remove from end if followed by a space - if (sortable.EndsWith(" " + search)) - { - sortable = sortable.Remove(sortable.Length - (search.Length + 1)); - } - } - - return ModifySortChunks(sortable); - } + public static ILibraryManager LibraryManager { get; set; } - private string ModifySortChunks(string name) - { - var chunks = GetSortChunks(name); + public static IServerConfigurationManager ConfigurationManager { get; set; } - var builder = new StringBuilder(); + public static IProviderManager ProviderManager { get; set; } - foreach (var chunk in chunks) - { - var chunkBuilder = chunk.Item1; + public static ILocalizationManager LocalizationManager { get; set; } - // This chunk is numeric - if (chunk.Item2) - { - while (chunkBuilder.Length < 10) - { - chunkBuilder.Insert(0, '0'); - } - } + public static IItemRepository ItemRepository { get; set; } - builder.Append(chunkBuilder); - } - //logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString()); - return builder.ToString().RemoveDiacritics(); - } + public static IFileSystem FileSystem { get; set; } - [IgnoreDataMember] - public bool EnableMediaSourceDisplay - { - get - { - if (SourceType == SourceType.Channel) - { - return ChannelManager.EnableMediaSourceDisplay(this); - } + public static IUserDataManager UserDataManager { get; set; } - return true; - } - } + public static IChannelManager ChannelManager { get; set; } - [IgnoreDataMember] - public Guid ParentId { get; set; } + public static IMediaSourceManager MediaSourceManager { get; set; } /// <summary> - /// Gets or sets the parent. + /// Gets or sets the name of the forced sort. /// </summary> - /// <value>The parent.</value> - [IgnoreDataMember] - public Folder Parent + /// <value>The name of the forced sort.</value> + [JsonIgnore] + public string ForcedSortName { - get => GetParent() as Folder; + get => _forcedSortName; set { - - } - } - - public void SetParent(Folder parent) - { - ParentId = parent == null ? Guid.Empty : parent.Id; - } - - public BaseItem GetParent() - { - var parentId = ParentId; - if (!parentId.Equals(Guid.Empty)) - { - return LibraryManager.GetItemById(parentId); - } - - return null; - } - - public IEnumerable<BaseItem> GetParents() - { - var parent = GetParent(); - - while (parent != null) - { - yield return parent; - - parent = parent.GetParent(); + _forcedSortName = value; + _sortName = null; } } /// <summary> - /// Finds a parent of a given type + /// Gets or sets the name of the sort. /// </summary> - /// <typeparam name="T"></typeparam> - /// <returns>``0.</returns> - public T FindParent<T>() - where T : Folder + /// <value>The name of the sort.</value> + [JsonIgnore] + public string SortName { - foreach (var parent in GetParents()) + get { - var item = parent as T; - if (item != null) + if (_sortName == null) { - return item; + if (!string.IsNullOrEmpty(ForcedSortName)) + { + // Need the ToLower because that's what CreateSortName does + _sortName = ModifySortChunks(ForcedSortName).ToLowerInvariant(); + } + else + { + _sortName = CreateSortName(); + } } + + return _sortName; } - return null; + + set => _sortName = value; } - [IgnoreDataMember] + [JsonIgnore] public virtual Guid DisplayParentId { get @@ -806,7 +592,7 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] public BaseItem DisplayParent { get @@ -816,126 +602,127 @@ namespace MediaBrowser.Controller.Entities { return null; } + return LibraryManager.GetItemById(id); } } /// <summary> - /// When the item first debuted. For movies this could be premiere date, episodes would be first aired + /// Gets or sets the date that the item first debuted. For movies this could be premiere date, episodes would be first aired. /// </summary> /// <value>The premiere date.</value> - [IgnoreDataMember] + [JsonIgnore] public DateTime? PremiereDate { get; set; } /// <summary> /// Gets or sets the end date. /// </summary> /// <value>The end date.</value> - [IgnoreDataMember] + [JsonIgnore] public DateTime? EndDate { get; set; } /// <summary> /// Gets or sets the official rating. /// </summary> /// <value>The official rating.</value> - [IgnoreDataMember] + [JsonIgnore] public string OfficialRating { get; set; } - [IgnoreDataMember] + [JsonIgnore] public int InheritedParentalRatingValue { get; set; } /// <summary> /// Gets or sets the critic rating. /// </summary> /// <value>The critic rating.</value> - [IgnoreDataMember] + [JsonIgnore] public float? CriticRating { get; set; } /// <summary> /// Gets or sets the custom rating. /// </summary> /// <value>The custom rating.</value> - [IgnoreDataMember] + [JsonIgnore] public string CustomRating { get; set; } /// <summary> /// Gets or sets the overview. /// </summary> /// <value>The overview.</value> - [IgnoreDataMember] + [JsonIgnore] public string Overview { get; set; } /// <summary> /// Gets or sets the studios. /// </summary> /// <value>The studios.</value> - [IgnoreDataMember] + [JsonIgnore] public string[] Studios { get; set; } /// <summary> /// Gets or sets the genres. /// </summary> /// <value>The genres.</value> - [IgnoreDataMember] + [JsonIgnore] public string[] Genres { get; set; } /// <summary> /// Gets or sets the tags. /// </summary> /// <value>The tags.</value> - [IgnoreDataMember] + [JsonIgnore] public string[] Tags { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string[] ProductionLocations { get; set; } /// <summary> /// Gets or sets the home page URL. /// </summary> /// <value>The home page URL.</value> - [IgnoreDataMember] + [JsonIgnore] public string HomePageUrl { get; set; } /// <summary> /// Gets or sets the community rating. /// </summary> /// <value>The community rating.</value> - [IgnoreDataMember] + [JsonIgnore] public float? CommunityRating { get; set; } /// <summary> /// Gets or sets the run time ticks. /// </summary> /// <value>The run time ticks.</value> - [IgnoreDataMember] + [JsonIgnore] public long? RunTimeTicks { get; set; } /// <summary> /// Gets or sets the production year. /// </summary> /// <value>The production year.</value> - [IgnoreDataMember] + [JsonIgnore] public int? ProductionYear { get; set; } /// <summary> - /// If the item is part of a series, this is it's number in the series. + /// Gets or sets the index number. If the item is part of a series, this is it's number in the series. /// This could be episode number, album track number, etc. /// </summary> /// <value>The index number.</value> - [IgnoreDataMember] + [JsonIgnore] public int? IndexNumber { get; set; } /// <summary> - /// For an episode this could be the season number, or for a song this could be the disc number. + /// Gets or sets the parent index number. For an episode this could be the season number, or for a song this could be the disc number. /// </summary> /// <value>The parent index number.</value> - [IgnoreDataMember] + [JsonIgnore] public int? ParentIndexNumber { get; set; } - [IgnoreDataMember] + [JsonIgnore] public virtual bool HasLocalAlternateVersions => false; - [IgnoreDataMember] + [JsonIgnore] public string OfficialRatingForComparison { get @@ -956,7 +743,7 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] public string CustomRatingForComparison { get @@ -978,21 +765,364 @@ namespace MediaBrowser.Controller.Entities } /// <summary> + /// Gets or sets the provider ids. + /// </summary> + /// <value>The provider ids.</value> + [JsonIgnore] + public Dictionary<string, string> ProviderIds { get; set; } + + [JsonIgnore] + public virtual Folder LatestItemsIndexContainer => null; + + [JsonIgnore] + public string PresentationUniqueKey { get; set; } + + [JsonIgnore] + public virtual bool EnableRememberingTrackSelections => true; + + [JsonIgnore] + public virtual bool IsTopParent + { + get + { + if (this is BasePluginFolder || this is Channel) + { + return true; + } + + if (this is IHasCollectionType view) + { + if (string.Equals(view.CollectionType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + if (GetParent() is AggregateFolder) + { + return true; + } + + return false; + } + } + + [JsonIgnore] + public virtual bool SupportsAncestors => true; + + [JsonIgnore] + public virtual bool StopRefreshIfLocalMetadataFound => true; + + [JsonIgnore] + protected virtual bool SupportsOwnedItems => !ParentId.Equals(Guid.Empty) && IsFileProtocol; + + [JsonIgnore] + public virtual bool SupportsPeople => false; + + [JsonIgnore] + public virtual bool SupportsThemeMedia => false; + + [JsonIgnore] + public virtual bool SupportsInheritedParentImages => false; + + /// <summary> + /// Gets a value indicating whether this instance is folder. + /// </summary> + /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value> + [JsonIgnore] + public virtual bool IsFolder => false; + + [JsonIgnore] + public virtual bool IsDisplayedAsFolder => false; + + /// <summary> + /// Gets or sets the remote trailers. + /// </summary> + /// <value>The remote trailers.</value> + public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; } + + public virtual bool SupportsExternalTransfer => false; + + public virtual double GetDefaultPrimaryImageAspectRatio() + { + return 0; + } + + public virtual string CreatePresentationUniqueKey() + { + return Id.ToString("N", CultureInfo.InvariantCulture); + } + + public bool IsPathProtocol(MediaProtocol protocol) + { + var current = PathProtocol; + + return current.HasValue && current.Value == protocol; + } + + private List<Tuple<StringBuilder, bool>> GetSortChunks(string s1) + { + var list = new List<Tuple<StringBuilder, bool>>(); + + int thisMarker = 0; + + while (thisMarker < s1.Length) + { + char thisCh = s1[thisMarker]; + + var thisChunk = new StringBuilder(); + bool isNumeric = char.IsDigit(thisCh); + + while (thisMarker < s1.Length && char.IsDigit(thisCh) == isNumeric) + { + thisChunk.Append(thisCh); + thisMarker++; + + if (thisMarker < s1.Length) + { + thisCh = s1[thisMarker]; + } + } + + list.Add(new Tuple<StringBuilder, bool>(thisChunk, isNumeric)); + } + + return list; + } + + public virtual bool CanDelete() + { + if (SourceType == SourceType.Channel) + { + return ChannelManager.CanDelete(this); + } + + return IsFileProtocol; + } + + public virtual bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders) + { + if (user.HasPermission(PermissionKind.EnableContentDeletion)) + { + return true; + } + + var allowed = user.GetPreferenceValues<Guid>(PreferenceKind.EnableContentDeletionFromFolders); + + if (SourceType == SourceType.Channel) + { + return allowed.Contains(ChannelId); + } + else + { + var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders); + + foreach (var folder in collectionFolders) + { + if (allowed.Contains(folder.Id)) + { + return true; + } + } + } + + return false; + } + + public BaseItem GetOwner() + { + var ownerId = OwnerId; + return ownerId.Equals(Guid.Empty) ? null : LibraryManager.GetItemById(ownerId); + } + + public bool CanDelete(User user, List<Folder> allCollectionFolders) + { + return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders); + } + + public bool CanDelete(User user) + { + var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList(); + + return CanDelete(user, allCollectionFolders); + } + + public virtual bool CanDownload() + { + return false; + } + + public virtual bool IsAuthorizedToDownload(User user) + { + return user.HasPermission(PermissionKind.EnableContentDownloading); + } + + public bool CanDownload(User user) + { + return CanDownload() && IsAuthorizedToDownload(user); + } + + /// <summary> + /// Returns a <see cref="string" /> that represents this instance. + /// </summary> + /// <returns>A <see cref="string" /> that represents this instance.</returns> + public override string ToString() + { + return Name; + } + + public string GetInternalMetadataPath() + { + var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath; + + return GetInternalMetadataPath(basePath); + } + + protected virtual string GetInternalMetadataPath(string basePath) + { + if (SourceType == SourceType.Channel) + { + return System.IO.Path.Join(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), Id.ToString("N", CultureInfo.InvariantCulture)); + } + + ReadOnlySpan<char> idString = Id.ToString("N", CultureInfo.InvariantCulture); + + return System.IO.Path.Join(basePath, "library", idString.Slice(0, 2), idString); + } + + /// <summary> + /// Creates the name of the sort. + /// </summary> + /// <returns>System.String.</returns> + protected virtual string CreateSortName() + { + if (Name == null) + { + return null; // some items may not have name filled in properly + } + + if (!EnableAlphaNumericSorting) + { + return Name.TrimStart(); + } + + var sortable = Name.Trim().ToLowerInvariant(); + + foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters) + { + sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal); + } + + foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters) + { + sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal); + } + + foreach (var search in ConfigurationManager.Configuration.SortRemoveWords) + { + // Remove from beginning if a space follows + if (sortable.StartsWith(search + " ", StringComparison.Ordinal)) + { + sortable = sortable.Remove(0, search.Length + 1); + } + + // Remove from middle if surrounded by spaces + sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal); + + // Remove from end if followed by a space + if (sortable.EndsWith(" " + search, StringComparison.Ordinal)) + { + sortable = sortable.Remove(sortable.Length - (search.Length + 1)); + } + } + + return ModifySortChunks(sortable); + } + + private string ModifySortChunks(string name) + { + var chunks = GetSortChunks(name); + + var builder = new StringBuilder(); + + foreach (var chunk in chunks) + { + var chunkBuilder = chunk.Item1; + + // This chunk is numeric + if (chunk.Item2) + { + while (chunkBuilder.Length < 10) + { + chunkBuilder.Insert(0, '0'); + } + } + + builder.Append(chunkBuilder); + } + + // logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString()); + return builder.ToString().RemoveDiacritics(); + } + + public BaseItem GetParent() + { + var parentId = ParentId; + if (!parentId.Equals(Guid.Empty)) + { + return LibraryManager.GetItemById(parentId); + } + + return null; + } + + public IEnumerable<BaseItem> GetParents() + { + var parent = GetParent(); + + while (parent != null) + { + yield return parent; + + parent = parent.GetParent(); + } + } + + /// <summary> + /// Finds a parent of a given type. + /// </summary> + /// <typeparam name="T">Type of parent.</typeparam> + /// <returns>``0.</returns> + public T FindParent<T>() + where T : Folder + { + foreach (var parent in GetParents()) + { + if (parent is T item) + { + return item; + } + } + + return null; + } + + /// <summary> /// Gets the play access. /// </summary> /// <param name="user">The user.</param> /// <returns>PlayAccess.</returns> public PlayAccess GetPlayAccess(User user) { - if (!user.Policy.EnableMediaPlayback) + if (!user.HasPermission(PermissionKind.EnableMediaPlayback)) { return PlayAccess.None; } - //if (!user.IsParentalScheduleAllowed()) - //{ + // if (!user.IsParentalScheduleAllowed()) + // { // return PlayAccess.None; - //} + // } return PlayAccess.Full; } @@ -1042,7 +1172,6 @@ namespace MediaBrowser.Controller.Entities } return 1; - }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0) .ThenByDescending(i => { @@ -1069,9 +1198,10 @@ namespace MediaBrowser.Controller.Entities var info = new MediaSourceInfo { - Id = item.Id.ToString("N"), + Id = item.Id.ToString("N", CultureInfo.InvariantCulture), Protocol = protocol ?? MediaProtocol.File, MediaStreams = MediaSourceManager.GetMediaStreams(item.Id), + MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id), Name = GetMediaSourceName(item), Path = enablePathSubstitution ? GetMappedPath(item, item.Path, protocol) : item.Path, RunTimeTicks = item.RunTimeTicks, @@ -1087,7 +1217,7 @@ namespace MediaBrowser.Controller.Entities if (info.Protocol == MediaProtocol.File) { - info.ETag = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N"); + info.ETag = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N", CultureInfo.InvariantCulture); } var video = item as Video; @@ -1192,11 +1322,11 @@ namespace MediaBrowser.Controller.Entities { if (video.IsoType.HasValue) { - if (video.IsoType.Value == Model.Entities.IsoType.BluRay) + if (video.IsoType.Value == IsoType.BluRay) { terms.Add("Bluray"); } - else if (video.IsoType.Value == Model.Entities.IsoType.Dvd) + else if (video.IsoType.Value == IsoType.Dvd) { terms.Add("DVD"); } @@ -1208,7 +1338,7 @@ namespace MediaBrowser.Controller.Entities } } - return string.Join("/", terms.ToArray()); + return string.Join('/', terms.ToArray()); } /// <summary> @@ -1224,8 +1354,7 @@ namespace MediaBrowser.Controller.Entities // Support plex/xbmc convention files.AddRange(fileSystemChildren - .Where(i => !i.IsDirectory && string.Equals(FileSystem.GetFileNameWithoutExtension(i), ThemeSongFilename, StringComparison.OrdinalIgnoreCase)) - ); + .Where(i => !i.IsDirectory && System.IO.Path.GetFileNameWithoutExtension(i.FullName.AsSpan()).Equals(ThemeSongFileName, StringComparison.OrdinalIgnoreCase))); return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions()) .OfType<Audio.Audio>() @@ -1265,16 +1394,15 @@ namespace MediaBrowser.Controller.Entities .Select(item => { // Try to retrieve it from the db. If we don't find it, use the resolved version - var dbItem = LibraryManager.GetItemById(item.Id) as Video; - if (dbItem != null) + if (LibraryManager.GetItemById(item.Id) is Video dbItem) { item = dbItem; } else { // item is new - item.ExtraType = MediaBrowser.Model.Entities.ExtraType.ThemeVideo; + item.ExtraType = Model.Entities.ExtraType.ThemeVideo; } return item; @@ -1285,54 +1413,45 @@ namespace MediaBrowser.Controller.Entities protected virtual BaseItem[] LoadExtras(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) { - var files = fileSystemChildren.Where(i => i.IsDirectory) - .SelectMany(i => FileSystem.GetFiles(i.FullName)); - - return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions()) - .OfType<Video>() - .Select(item => - { - // Try to retrieve it from the db. If we don't find it, use the resolved version - var dbItem = LibraryManager.GetItemById(item.Id) as Video; - - if (dbItem != null) - { - item = dbItem; - } - else + return fileSystemChildren + .Where(child => child.IsDirectory && AllExtrasTypesFolderNames.ContainsKey(child.Name)) + .SelectMany(folder => LibraryManager + .ResolvePaths(FileSystem.GetFiles(folder.FullName), directoryService, null, new LibraryOptions()) + .OfType<Video>() + .Select(video => { - // item is new - item.ExtraType = MediaBrowser.Model.Entities.ExtraType.Clip; - } - - return item; + // Try to retrieve it from the db. If we don't find it, use the resolved version + if (LibraryManager.GetItemById(video.Id) is Video dbItem) + { + video = dbItem; + } - // Sort them so that the list can be easily compared for changes - }).OrderBy(i => i.Path).ToArray(); + video.ExtraType = AllExtrasTypesFolderNames[folder.Name]; + return video; + }) + .OrderBy(video => video.Path)) // Sort them so that the list can be easily compared for changes + .ToArray(); } - public Task RefreshMetadata(CancellationToken cancellationToken) { - return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)), cancellationToken); + return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken); } protected virtual void TriggerOnRefreshStart() { - } protected virtual void TriggerOnRefreshComplete() { - } /// <summary> - /// Overrides the base implementation to refresh metadata for local trailers + /// Overrides the base implementation to refresh metadata for local trailers. /// </summary> /// <param name="options">The options.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>true if a provider reports we changed</returns> + /// <returns>true if a provider reports we changed.</returns> public async Task<ItemUpdateType> RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellationToken) { TriggerOnRefreshStart(); @@ -1348,6 +1467,7 @@ namespace MediaBrowser.Controller.Entities new List<FileSystemMetadata>(); var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false); + await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties in DB are fresh if (ownedItemsChanged) { @@ -1377,23 +1497,55 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] - protected virtual bool SupportsOwnedItems => !ParentId.Equals(Guid.Empty) && IsFileProtocol; + protected bool IsVisibleStandaloneInternal(User user, bool checkFolders) + { + if (!IsVisible(user)) + { + return false; + } - [IgnoreDataMember] - public virtual bool SupportsPeople => false; + if (GetParents().Any(i => !i.IsVisible(user))) + { + return false; + } - [IgnoreDataMember] - public virtual bool SupportsThemeMedia => false; + if (checkFolders) + { + var topParent = GetParents().LastOrDefault() ?? this; + + if (string.IsNullOrEmpty(topParent.Path)) + { + return true; + } + + var itemCollectionFolders = LibraryManager.GetCollectionFolders(this).Select(i => i.Id).ToList(); + + if (itemCollectionFolders.Count > 0) + { + var userCollectionFolders = LibraryManager.GetUserRootFolder().GetChildren(user, true).Select(i => i.Id).ToList(); + if (!itemCollectionFolders.Any(userCollectionFolders.Contains)) + { + return false; + } + } + } + + return true; + } + + public void SetParent(Folder parent) + { + ParentId = parent == null ? Guid.Empty : parent.Id; + } /// <summary> /// Refreshes owned items such as trailers, theme videos, special features, etc. /// Returns true or false indicating if changes were found. /// </summary> - /// <param name="options"></param> - /// <param name="fileSystemChildren"></param> - /// <param name="cancellationToken"></param> - /// <returns></returns> + /// <param name="options">The metadata refresh options.</param> + /// <param name="fileSystemChildren">The list of filesystem children.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns><c>true</c> if any items have changed, else <c>false</c>.</returns> protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) { var themeSongsChanged = false; @@ -1470,7 +1622,13 @@ namespace MediaBrowser.Controller.Entities private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) { - var newExtras = LoadExtras(fileSystemChildren, options.DirectoryService).Concat(LoadThemeVideos(fileSystemChildren, options.DirectoryService)).Concat(LoadThemeSongs(fileSystemChildren, options.DirectoryService)); + var extras = LoadExtras(fileSystemChildren, options.DirectoryService); + var themeVideos = LoadThemeVideos(fileSystemChildren, options.DirectoryService); + var themeSongs = LoadThemeSongs(fileSystemChildren, options.DirectoryService); + var newExtras = new BaseItem[extras.Length + themeVideos.Length + themeSongs.Length]; + extras.CopyTo(newExtras, 0); + themeVideos.CopyTo(newExtras, extras.Length); + themeSongs.CopyTo(newExtras, extras.Length + themeVideos.Length); var newExtraIds = newExtras.Select(i => i.Id).ToArray(); @@ -1482,7 +1640,15 @@ namespace MediaBrowser.Controller.Entities var tasks = newExtras.Select(i => { - return RefreshMetadataForOwnedItem(i, true, new MetadataRefreshOptions(options), cancellationToken); + var subOptions = new MetadataRefreshOptions(options); + if (i.OwnerId != ownerId || i.ParentId != Guid.Empty) + { + i.OwnerId = ownerId; + i.ParentId = Guid.Empty; + subOptions.ForceSave = true; + } + + return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken); }); await Task.WhenAll(tasks).ConfigureAwait(false); @@ -1523,7 +1689,8 @@ namespace MediaBrowser.Controller.Entities await Task.WhenAll(tasks).ConfigureAwait(false); - item.ThemeVideoIds = newThemeVideoIds; + // They are expected to be sorted by SortName + item.ThemeVideoIds = newThemeVideos.OrderBy(i => i.SortName).Select(i => i.Id).ToArray(); return themeVideosChanged; } @@ -1560,34 +1727,12 @@ namespace MediaBrowser.Controller.Entities await Task.WhenAll(tasks).ConfigureAwait(false); - item.ThemeSongIds = newThemeSongIds; + // They are expected to be sorted by SortName + item.ThemeSongIds = newThemeSongs.OrderBy(i => i.SortName).Select(i => i.Id).ToArray(); return themeSongsChanged; } - /// <summary> - /// Gets or sets the provider ids. - /// </summary> - /// <value>The provider ids.</value> - [IgnoreDataMember] - public Dictionary<string, string> ProviderIds { get; set; } - - [IgnoreDataMember] - public virtual Folder LatestItemsIndexContainer => null; - - public virtual double GetDefaultPrimaryImageAspectRatio() - { - return 0; - } - - public virtual string CreatePresentationUniqueKey() - { - return Id.ToString("N"); - } - - [IgnoreDataMember] - public string PresentationUniqueKey { get; set; } - public string GetPresentationUniqueKey() { return PresentationUniqueKey ?? CreatePresentationUniqueKey(); @@ -1715,11 +1860,11 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Determines if a given user has access to this item + /// Determines if a given user has access to this item. /// </summary> /// <param name="user">The user.</param> /// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns> - /// <exception cref="ArgumentNullException">user</exception> + /// <exception cref="ArgumentNullException">If user is null.</exception> public bool IsParentalAllowed(User user) { if (user == null) @@ -1732,7 +1877,7 @@ namespace MediaBrowser.Controller.Entities return false; } - var maxAllowedRating = user.Policy.MaxParentalRating; + var maxAllowedRating = user.MaxParentalAgeRating; if (maxAllowedRating == null) { @@ -1748,7 +1893,7 @@ namespace MediaBrowser.Controller.Entities if (string.IsNullOrEmpty(rating)) { - return !GetBlockUnratedValue(user.Policy); + return !GetBlockUnratedValue(user); } var value = LocalizationManager.GetRatingLevel(rating); @@ -1756,7 +1901,7 @@ namespace MediaBrowser.Controller.Entities // Could not determine the integer value if (!value.HasValue) { - var isAllowed = !GetBlockUnratedValue(user.Policy); + var isAllowed = !GetBlockUnratedValue(user); if (!isAllowed) { @@ -1818,8 +1963,7 @@ namespace MediaBrowser.Controller.Entities private bool IsVisibleViaTags(User user) { - var policy = user.Policy; - if (policy.BlockedTags.Any(i => Tags.Contains(i, StringComparer.OrdinalIgnoreCase))) + if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => Tags.Contains(i, StringComparer.OrdinalIgnoreCase))) { return false; } @@ -1845,22 +1989,18 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Gets the block unrated value. /// </summary> - /// <param name="config">The configuration.</param> + /// <param name="user">The configuration.</param> /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> - protected virtual bool GetBlockUnratedValue(UserPolicy config) + protected virtual bool GetBlockUnratedValue(User user) { // Don't block plain folders that are unrated. Let the media underneath get blocked // Special folders like series and albums will override this method. - if (IsFolder) - { - return false; - } - if (this is IItemByName) + if (IsFolder || this is IItemByName) { return false; } - return config.BlockUnratedItems.Contains(GetBlockUnratedType()); + return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType()); } /// <summary> @@ -1869,7 +2009,7 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <param name="user">The user.</param> /// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns> - /// <exception cref="ArgumentNullException">user</exception> + /// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception> public virtual bool IsVisible(User user) { if (user == null) @@ -1890,58 +2030,9 @@ namespace MediaBrowser.Controller.Entities return IsVisibleStandaloneInternal(user, true); } - [IgnoreDataMember] - public virtual bool SupportsInheritedParentImages => false; - - protected bool IsVisibleStandaloneInternal(User user, bool checkFolders) - { - if (!IsVisible(user)) - { - return false; - } - - if (GetParents().Any(i => !i.IsVisible(user))) - { - return false; - } - - if (checkFolders) - { - var topParent = GetParents().LastOrDefault() ?? this; - - if (string.IsNullOrEmpty(topParent.Path)) - { - return true; - } - - var itemCollectionFolders = LibraryManager.GetCollectionFolders(this).Select(i => i.Id).ToList(); - - if (itemCollectionFolders.Count > 0) - { - var userCollectionFolders = LibraryManager.GetUserRootFolder().GetChildren(user, true).Select(i => i.Id).ToList(); - if (!itemCollectionFolders.Any(userCollectionFolders.Contains)) - { - return false; - } - } - } - - return true; - } - - /// <summary> - /// Gets a value indicating whether this instance is folder. - /// </summary> - /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value> - [IgnoreDataMember] - public virtual bool IsFolder => false; - - [IgnoreDataMember] - public virtual bool IsDisplayedAsFolder => false; - public virtual string GetClientTypeName() { - if (IsFolder && SourceType == SourceType.Channel && !(this is Channel)) + if (IsFolder && SourceType == SourceType.Channel && this is not Channel) { return "ChannelFolderItem"; } @@ -1949,6 +2040,11 @@ namespace MediaBrowser.Controller.Entities return GetType().Name; } + public BaseItemKind GetBaseItemKind() + { + return Enum.Parse<BaseItemKind>(GetClientTypeName()); + } + /// <summary> /// Gets the linked child. /// </summary> @@ -2001,7 +2097,7 @@ namespace MediaBrowser.Controller.Entities if (itemByPath == null) { - //Logger.LogWarning("Unable to find linked item at path {0}", info.Path); + Logger.LogWarning("Unable to find linked item at path {0}", info.Path); } return itemByPath; @@ -2013,7 +2109,7 @@ namespace MediaBrowser.Controller.Entities if (item == null) { - //Logger.LogWarning("Unable to find linked item at path {0}", info.Path); + Logger.LogWarning("Unable to find linked item at path {0}", info.Path); } return item; @@ -2022,14 +2118,11 @@ namespace MediaBrowser.Controller.Entities return null; } - [IgnoreDataMember] - public virtual bool EnableRememberingTrackSelections => true; - /// <summary> - /// Adds a studio to the item + /// Adds a studio to the item. /// </summary> /// <param name="name">The name.</param> - /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentNullException">Throws if name is null.</exception> public void AddStudio(string name) { if (string.IsNullOrEmpty(name)) @@ -2041,14 +2134,17 @@ namespace MediaBrowser.Controller.Entities if (!current.Contains(name, StringComparer.OrdinalIgnoreCase)) { - if (current.Length == 0) + int curLen = current.Length; + if (curLen == 0) { Studios = new[] { name }; } else { - var list = - Studios = current.Concat(new[] { name }).ToArray(); + var newArr = new string[curLen + 1]; + current.CopyTo(newArr, 0); + newArr[curLen] = name; + Studios = newArr; } } } @@ -2059,10 +2155,10 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Adds a genre to the item + /// Adds a genre to the item. /// </summary> /// <param name="name">The name.</param> - /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentNullException">Throwns if name is null.</exception> public void AddGenre(string name) { if (string.IsNullOrEmpty(name)) @@ -2085,9 +2181,9 @@ namespace MediaBrowser.Controller.Entities /// <param name="user">The user.</param> /// <param name="datePlayed">The date played.</param> /// <param name="resetPosition">if set to <c>true</c> [reset position].</param> - /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException"></exception> - public virtual void MarkPlayed(User user, + /// <exception cref="ArgumentNullException">Throws if user is null.</exception> + public virtual void MarkPlayed( + User user, DateTime? datePlayed, bool resetPosition) { @@ -2122,8 +2218,7 @@ namespace MediaBrowser.Controller.Entities /// Marks the unplayed. /// </summary> /// <param name="user">The user.</param> - /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentNullException">Throws if user is null.</exception> public virtual void MarkUnplayed(User user) { if (user == null) @@ -2133,7 +2228,7 @@ namespace MediaBrowser.Controller.Entities var data = UserDataManager.GetUserData(user, this); - //I think it is okay to do this here. + // I think it is okay to do this here. // if this is only called when a user is manually forcing something to un-played // then it probably is what we want to do... data.PlayCount = 0; @@ -2147,22 +2242,18 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed. /// </summary> - /// <returns>Task.</returns> public virtual void ChangedExternally() { - ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)) - { - - }, RefreshPriority.High); + ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(FileSystem)), RefreshPriority.High); } /// <summary> - /// Gets an image + /// Gets an image. /// </summary> /// <param name="type">The type.</param> /// <param name="imageIndex">Index of the image.</param> /// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns> - /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops</exception> + /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops.</exception> public bool HasImage(ImageType type, int imageIndex) { return GetImageInfo(type, imageIndex) != null; @@ -2183,12 +2274,16 @@ namespace MediaBrowser.Controller.Entities existingImage.DateModified = image.DateModified; existingImage.Width = image.Width; existingImage.Height = image.Height; + existingImage.BlurHash = image.BlurHash; } - else { - var currentCount = ImageInfos.Length; - ImageInfos = ImageInfos.Concat(new[] { image }).ToArray(); + var current = ImageInfos; + var currentCount = current.Length; + var newArr = new ItemImageInfo[currentCount + 1]; + current.CopyTo(newArr, 0); + newArr[currentCount] = image; + ImageInfos = newArr; } } @@ -2223,8 +2318,8 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <param name="type">The type.</param> /// <param name="index">The index.</param> - /// <returns>Task.</returns> - public void DeleteImage(ImageType type, int index) + /// <returns>A task.</returns> + public async Task DeleteImageAsync(ImageType type, int index) { var info = GetImageInfo(type, index); @@ -2242,7 +2337,7 @@ namespace MediaBrowser.Controller.Entities FileSystem.DeleteFile(info.Path); } - UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); + await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); } public void RemoveImage(ItemImageInfo image) @@ -2255,21 +2350,21 @@ namespace MediaBrowser.Controller.Entities ImageInfos = ImageInfos.Except(deletedImages).ToArray(); } - public virtual void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken) - { - LibraryManager.UpdateItem(this, GetParent(), updateReason, cancellationToken); - } + public virtual Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken) + => LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken); /// <summary> - /// Validates that images within the item are still on the file system + /// Validates that images within the item are still on the filesystem. /// </summary> + /// <param name="directoryService">The directory service to use.</param> + /// <returns><c>true</c> if the images validate, <c>false</c> if not.</returns> public bool ValidateImages(IDirectoryService directoryService) { var allFiles = ImageInfos .Where(i => i.IsLocalFile) .Select(i => System.IO.Path.GetDirectoryName(i.Path)) .Distinct(StringComparer.OrdinalIgnoreCase) - .SelectMany(i => directoryService.GetFilePaths(i)) + .SelectMany(path => directoryService.GetFilePaths(path)) .ToList(); var deletedImages = ImageInfos @@ -2290,9 +2385,7 @@ namespace MediaBrowser.Controller.Entities /// <param name="imageType">Type of the image.</param> /// <param name="imageIndex">Index of the image.</param> /// <returns>System.String.</returns> - /// <exception cref="InvalidOperationException"> - /// </exception> - /// <exception cref="ArgumentNullException">item</exception> + /// <exception cref="ArgumentNullException">Item is null.</exception> public string GetImagePath(ImageType imageType, int imageIndex) => GetImageInfo(imageType, imageIndex)?.Path; @@ -2328,10 +2421,61 @@ namespace MediaBrowser.Controller.Entities }; } + // Music albums usually don't have dedicated backdrops, so return one from the artist instead + if (GetType() == typeof(MusicAlbum) && imageType == ImageType.Backdrop) + { + var artist = FindParent<MusicArtist>(); + + if (artist != null) + { + return artist.GetImages(imageType).ElementAtOrDefault(imageIndex); + } + } + return GetImages(imageType) .ElementAtOrDefault(imageIndex); } + /// <summary> + /// Computes image index for given image or raises if no matching image found. + /// </summary> + /// <param name="image">Image to compute index for.</param> + /// <exception cref="ArgumentException">Image index cannot be computed as no matching image found. + /// </exception> + /// <returns>Image index.</returns> + public int GetImageIndex(ItemImageInfo image) + { + if (image == null) + { + throw new ArgumentNullException(nameof(image)); + } + + if (image.Type == ImageType.Chapter) + { + var chapters = ItemRepository.GetChapters(this); + for (var i = 0; i < chapters.Count; i++) + { + if (chapters[i].ImagePath == image.Path) + { + return i; + } + } + + throw new ArgumentException("No chapter index found for image path", image.Path); + } + + var images = GetImages(image.Type).ToArray(); + for (var i = 0; i < images.Length; i++) + { + if (images[i].Path == image.Path) + { + return i; + } + } + + throw new ArgumentException("No image index found for image path", image.Path); + } + public IEnumerable<ItemImageInfo> GetImages(ImageType imageType) { if (imageType == ImageType.Chapter) @@ -2339,7 +2483,15 @@ namespace MediaBrowser.Controller.Entities throw new ArgumentException("No image info for chapter images"); } - return ImageInfos.Where(i => i.Type == imageType); + // Yield return is more performant than LINQ Where on an Array + for (var i = 0; i < ImageInfos.Length; i++) + { + var imageInfo = ImageInfos[i]; + if (imageInfo.Type == imageType) + { + yield return imageInfo; + } + } } /// <summary> @@ -2348,7 +2500,7 @@ namespace MediaBrowser.Controller.Entities /// <param name="imageType">Type of the image.</param> /// <param name="images">The images.</param> /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> - /// <exception cref="ArgumentException">Cannot call AddImages with chapter images</exception> + /// <exception cref="ArgumentException">Cannot call AddImages with chapter images.</exception> public bool AddImages(ImageType imageType, List<FileSystemMetadata> images) { if (imageType == ImageType.Chapter) @@ -2371,7 +2523,7 @@ namespace MediaBrowser.Controller.Entities } var existing = existingImages - .FirstOrDefault(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase)); + .Find(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase)); if (existing == null) { @@ -2402,8 +2554,7 @@ namespace MediaBrowser.Controller.Entities var newImagePaths = images.Select(i => i.FullName).ToList(); var deleted = existingImages - .Where(i => i.IsLocalFile && !newImagePaths.Contains(i.Path, StringComparer.OrdinalIgnoreCase) && !File.Exists(i.Path)) - .ToList(); + .FindAll(i => i.IsLocalFile && !newImagePaths.Contains(i.Path.AsSpan(), StringComparison.OrdinalIgnoreCase) && !File.Exists(i.Path)); if (deleted.Count > 0) { @@ -2430,12 +2581,13 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Gets the file system path to delete when the item is to be deleted + /// Gets the file system path to delete when the item is to be deleted. /// </summary> - /// <returns></returns> + /// <returns>The metadata for the deleted paths.</returns> public virtual IEnumerable<FileSystemMetadata> GetDeletePaths() { - return new[] { + return new[] + { new FileSystemMetadata { FullName = Path, @@ -2452,10 +2604,8 @@ namespace MediaBrowser.Controller.Entities } var filename = System.IO.Path.GetFileNameWithoutExtension(Path); - var extensions = new List<string> { ".nfo", ".xml", ".srt", ".vtt", ".sub", ".idx", ".txt", ".edl", ".bif", ".smi", ".ttml" }; - extensions.AddRange(SupportedImageExtensions); - return FileSystem.GetFiles(System.IO.Path.GetDirectoryName(Path), extensions.ToArray(), false, false) + return FileSystem.GetFiles(System.IO.Path.GetDirectoryName(Path), _supportedExtensions, false, false) .Where(i => System.IO.Path.GetFileNameWithoutExtension(i.FullName).StartsWith(filename, StringComparison.OrdinalIgnoreCase)) .ToList(); } @@ -2465,11 +2615,11 @@ namespace MediaBrowser.Controller.Entities return type == ImageType.Backdrop || type == ImageType.Screenshot || type == ImageType.Chapter; } - public void SwapImages(ImageType type, int index1, int index2) + public Task SwapImagesAsync(ImageType type, int index1, int index2) { if (!AllowsMultipleImages(type)) { - throw new ArgumentException("The change index operation is only applicable to backdrops and screenshots"); + throw new ArgumentException("The change index operation is only applicable to backdrops and screen shots"); } var info1 = GetImageInfo(type, index1); @@ -2478,13 +2628,13 @@ namespace MediaBrowser.Controller.Entities if (info1 == null || info2 == null) { // Nothing to do - return; + return Task.CompletedTask; } if (!info1.IsLocalFile || !info2.IsLocalFile) { // TODO: Not supported yet - return; + return Task.CompletedTask; } var path1 = info1.Path; @@ -2501,7 +2651,7 @@ namespace MediaBrowser.Controller.Entities info2.Width = 0; info2.Height = 0; - UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); + return UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None); } public virtual bool IsPlayed(User user) @@ -2540,9 +2690,11 @@ namespace MediaBrowser.Controller.Entities { return new T { + Path = Path, MetadataCountryCode = GetPreferredMetadataCountryCode(), MetadataLanguage = GetPreferredMetadataLanguage(), Name = GetNameForMetadataLookup(), + OriginalTitle = OriginalTitle, ProviderIds = ProviderIds, IndexNumber = IndexNumber, ParentIndexNumber = ParentIndexNumber, @@ -2557,9 +2709,11 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true if changes were made. /// </summary> - public virtual bool BeforeMetadataRefresh(bool replaceAllMetdata) + /// <param name="replaceAllMetadata">Whether to replace all metadata.</param> + /// <returns>true if the item has change, else false.</returns> + public virtual bool BeforeMetadataRefresh(bool replaceAllMetadata) { _sortName = null; @@ -2617,36 +2771,43 @@ namespace MediaBrowser.Controller.Entities newOptions.ForceSave = true; ownedItem.Genres = item.Genres; } + if (!item.Studios.SequenceEqual(ownedItem.Studios, StringComparer.Ordinal)) { newOptions.ForceSave = true; ownedItem.Studios = item.Studios; } + if (!item.ProductionLocations.SequenceEqual(ownedItem.ProductionLocations, StringComparer.Ordinal)) { newOptions.ForceSave = true; ownedItem.ProductionLocations = item.ProductionLocations; } + if (item.CommunityRating != ownedItem.CommunityRating) { ownedItem.CommunityRating = item.CommunityRating; newOptions.ForceSave = true; } + if (item.CriticRating != ownedItem.CriticRating) { ownedItem.CriticRating = item.CriticRating; newOptions.ForceSave = true; } + if (!string.Equals(item.Overview, ownedItem.Overview, StringComparison.Ordinal)) { ownedItem.Overview = item.Overview; newOptions.ForceSave = true; } + if (!string.Equals(item.OfficialRating, ownedItem.OfficialRating, StringComparison.Ordinal)) { ownedItem.OfficialRating = item.OfficialRating; newOptions.ForceSave = true; } + if (!string.Equals(item.CustomRating, ownedItem.CustomRating, StringComparison.Ordinal)) { ownedItem.CustomRating = item.CustomRating; @@ -2674,13 +2835,13 @@ namespace MediaBrowser.Controller.Entities newOptions.ForceSave = true; } - //var parentId = Id; - //if (!video.IsOwnedItem || video.ParentId != parentId) - //{ + // var parentId = Id; + // if (!video.IsOwnedItem || video.ParentId != parentId) + // { // video.IsOwnedItem = true; // video.ParentId = parentId; // newOptions.ForceSave = true; - //} + // } if (video == null) { @@ -2694,7 +2855,7 @@ namespace MediaBrowser.Controller.Entities { var list = GetEtagValues(user); - return string.Join("|", list.ToArray()).GetMD5().ToString("N"); + return string.Join('|', list).GetMD5().ToString("N", CultureInfo.InvariantCulture); } protected virtual List<string> GetEtagValues(User user) @@ -2717,50 +2878,9 @@ namespace MediaBrowser.Controller.Entities return this; } - foreach (var parent in GetParents()) - { - if (parent.IsTopParent) - { - return parent; - } - } - return null; + return GetParents().FirstOrDefault(parent => parent.IsTopParent); } - [IgnoreDataMember] - public virtual bool IsTopParent - { - get - { - if (this is BasePluginFolder || this is Channel) - { - return true; - } - - var view = this as IHasCollectionType; - if (view != null) - { - if (string.Equals(view.CollectionType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - if (GetParent() is AggregateFolder) - { - return true; - } - - return false; - } - } - - [IgnoreDataMember] - public virtual bool SupportsAncestors => true; - - [IgnoreDataMember] - public virtual bool StopRefreshIfLocalMetadataFound => true; - public virtual IEnumerable<Guid> GetIdsForAncestorQuery() { return new[] { Id }; @@ -2795,7 +2915,8 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Updates the official rating based on content and returns true or false indicating if it changed. /// </summary> - /// <returns></returns> + /// <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) { var currentOfficialRating = OfficialRating; @@ -2811,55 +2932,74 @@ namespace MediaBrowser.Controller.Entities OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating; - return !string.Equals(currentOfficialRating ?? string.Empty, OfficialRating ?? string.Empty, + return !string.Equals( + currentOfficialRating ?? string.Empty, + OfficialRating ?? string.Empty, StringComparison.OrdinalIgnoreCase); } public IEnumerable<BaseItem> GetThemeSongs() { - return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeSong)).OrderBy(i => i.SortName); + return ThemeSongIds.Select(LibraryManager.GetItemById); } public IEnumerable<BaseItem> GetThemeVideos() { - return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeVideo)).OrderBy(i => i.SortName); + return ThemeVideoIds.Select(LibraryManager.GetItemById); } /// <summary> - /// Gets or sets the remote trailers. + /// Get all extras associated with this item, sorted by <see cref="SortName"/>. /// </summary> - /// <value>The remote trailers.</value> - public MediaUrl[] RemoteTrailers { get; set; } - + /// <returns>An enumerable containing the items.</returns> public IEnumerable<BaseItem> GetExtras() { - return ExtraIds.Select(LibraryManager.GetItemById).Where(i => i != null).OrderBy(i => i.SortName); + return ExtraIds + .Select(LibraryManager.GetItemById) + .Where(i => i != null) + .OrderBy(i => i.SortName); } - public IEnumerable<BaseItem> GetExtras(ExtraType[] extraTypes) + /// <summary> + /// Get all extras with specific types that are associated with this item. + /// </summary> + /// <param name="extraTypes">The types of extras to retrieve.</param> + /// <returns>An enumerable containing the extras.</returns> + public IEnumerable<BaseItem> GetExtras(IReadOnlyCollection<ExtraType> extraTypes) { - return ExtraIds.Select(LibraryManager.GetItemById).Where(i => i != null && extraTypes.Contains(i.ExtraType.Value)).OrderBy(i => i.SortName); + return ExtraIds + .Select(LibraryManager.GetItemById) + .Where(i => i != null) + .Where(i => i.ExtraType.HasValue && extraTypes.Contains(i.ExtraType.Value)); } - public IEnumerable<BaseItem> GetDisplayExtras() + public IEnumerable<BaseItem> GetTrailers() { - return GetExtras(DisplayExtraTypes); + if (this is IHasTrailers) + { + return ((IHasTrailers)this).LocalTrailerIds.Select(LibraryManager.GetItemById).Where(i => i != null).OrderBy(i => i.SortName); + } + else + { + return Array.Empty<BaseItem>(); + } } - public virtual bool IsHD => Height >= 720; - public bool IsShortcut { get; set; } - public string ShortcutPath { get; set; } - public int Width { get; set; } - public int Height { get; set; } - public Guid[] ExtraIds { get; set; } public virtual long GetRunTimeTicksForPlayState() { return RunTimeTicks ?? 0; } - // Possible types of extra videos - public static ExtraType[] DisplayExtraTypes = new[] { Model.Entities.ExtraType.BehindTheScenes, Model.Entities.ExtraType.Clip, Model.Entities.ExtraType.DeletedScene, Model.Entities.ExtraType.Interview, Model.Entities.ExtraType.Sample, Model.Entities.ExtraType.Scene }; + /// <inheritdoc /> + public override bool Equals(object obj) + { + return obj is BaseItem baseItem && this.Equals(baseItem); + } - public virtual bool SupportsExternalTransfer => false; + /// <inheritdoc /> + public bool Equals(BaseItem other) => Equals(Id, other?.Id); + + /// <inheritdoc /> + public override int GetHashCode() => HashCode.Combine(Id); } } diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs index 9c955a724..33870e2fb 100644 --- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs +++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 + +using System; using System.Linq; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; @@ -41,9 +44,10 @@ namespace MediaBrowser.Controller.Entities /// <param name="file">The file.</param> public static void SetImagePath(this BaseItem item, ImageType imageType, string file) { - if (file.StartsWith("http", System.StringComparison.OrdinalIgnoreCase)) + if (file.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { - item.SetImage(new ItemImageInfo + item.SetImage( + new ItemImageInfo { Path = file, Type = imageType @@ -60,40 +64,63 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <param name="source">The source object.</param> /// <param name="dest">The destination object.</param> + /// <typeparam name="T">Source type.</typeparam> + /// <typeparam name="TU">Destination type.</typeparam> public static void DeepCopy<T, TU>(this T source, TU dest) - where T : BaseItem - where TU : BaseItem + where T : BaseItem + where TU : BaseItem { - var sourceProps = typeof(T).GetProperties().Where(x => x.CanRead).ToList(); - var destProps = typeof(TU).GetProperties() - .Where(x => x.CanWrite) - .ToList(); + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (dest == null) + { + throw new ArgumentNullException(nameof(dest)); + } - foreach (var sourceProp in sourceProps) + var destProps = typeof(TU).GetProperties().Where(x => x.CanWrite).ToList(); + + foreach (var sourceProp in typeof(T).GetProperties()) { - if (destProps.Any(x => x.Name == sourceProp.Name)) + // We should be able to write to the property + // for both the source and destination type + // This is only false when the derived type hides the base member + // (which we shouldn't copy anyway) + if (!sourceProp.CanRead || !sourceProp.CanWrite) { - var p = destProps.First(x => x.Name == sourceProp.Name); - p.SetValue(dest, sourceProp.GetValue(source, null), null); + continue; } - } + var v = sourceProp.GetValue(source); + if (v == null) + { + continue; + } + var p = destProps.Find(x => x.Name == sourceProp.Name); + if (p != null) + { + p.SetValue(dest, v); + } + } } /// <summary> /// Copies all properties on newly created object. Skips properties that do not exist. /// </summary> /// <param name="source">The source object.</param> + /// <typeparam name="T">Source type.</typeparam> + /// <typeparam name="TU">Destination type.</typeparam> + /// <returns>Destination object.</returns> public static TU DeepCopy<T, TU>(this T source) - where T : BaseItem - where TU : BaseItem, new() + where T : BaseItem + where TU : BaseItem, new() { var dest = new TU(); source.DeepCopy(dest); return dest; } - - } } diff --git a/MediaBrowser.Controller/Entities/BasePluginFolder.cs b/MediaBrowser.Controller/Entities/BasePluginFolder.cs index 8cdb9695c..272a37df1 100644 --- a/MediaBrowser.Controller/Entities/BasePluginFolder.cs +++ b/MediaBrowser.Controller/Entities/BasePluginFolder.cs @@ -1,4 +1,8 @@ -using MediaBrowser.Model.Serialization; +#nullable disable + +#pragma warning disable CS1591 + +using System.Text.Json.Serialization; namespace MediaBrowser.Controller.Entities { @@ -8,9 +12,15 @@ namespace MediaBrowser.Controller.Entities /// </summary> public abstract class BasePluginFolder : Folder, ICollectionFolder { - [IgnoreDataMember] + [JsonIgnore] public virtual string CollectionType => null; + [JsonIgnore] + public override bool SupportsInheritedParentImages => false; + + [JsonIgnore] + public override bool SupportsPeople => false; + public override bool CanDelete() { return false; @@ -20,19 +30,5 @@ namespace MediaBrowser.Controller.Entities { return true; } - - [IgnoreDataMember] - public override bool SupportsInheritedParentImages => false; - - [IgnoreDataMember] - public override bool SupportsPeople => false; - - //public override double? GetDefaultPrimaryImageAspectRatio() - //{ - // double value = 16; - // value /= 9; - - // return value; - //} } } diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs index 7a23d9a66..d75beb06d 100644 --- a/MediaBrowser.Controller/Entities/Book.cs +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -1,31 +1,48 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Linq; +using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { public class Book : BaseItem, IHasLookupInfo<BookInfo>, IHasSeries { - [IgnoreDataMember] + public Book() + { + this.RunTimeTicks = TimeSpan.TicksPerSecond; + } + + [JsonIgnore] public override string MediaType => Model.Entities.MediaType.Book; - [IgnoreDataMember] + public override bool SupportsPlayedStatus => true; + + public override bool SupportsPositionTicksResume => true; + + [JsonIgnore] public string SeriesPresentationUniqueKey { get; set; } - [IgnoreDataMember] + + [JsonIgnore] public string SeriesName { get; set; } - [IgnoreDataMember] + + [JsonIgnore] public Guid SeriesId { get; set; } public string FindSeriesSortName() { return SeriesName; } + public string FindSeriesName() { return SeriesName; } + public string FindSeriesPresentationUniqueKey() { return SeriesPresentationUniqueKey; @@ -36,11 +53,13 @@ namespace MediaBrowser.Controller.Entities return SeriesId; } + /// <inheritdoc /> public override bool CanDownload() { return IsFileProtocol; } + /// <inheritdoc /> public override UnratedItem GetBlockUnratedType() { return UnratedItem.Book; diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index 275052d48..7dc7f774d 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -1,15 +1,20 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; - +using Jellyfin.Extensions.Json; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; @@ -19,34 +24,70 @@ namespace MediaBrowser.Controller.Entities { /// <summary> /// Specialized Folder class that points to a subset of the physical folders in the system. - /// It is created from the user-specific folders within the system root + /// It is created from the user-specific folders within the system root. /// </summary> public class CollectionFolder : Folder, ICollectionFolder { - public static IXmlSerializer XmlSerializer { get; set; } - public static IJsonSerializer JsonSerializer { get; set; } - public static IServerApplicationHost ApplicationHost { get; set; } + private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; + private static readonly Dictionary<string, LibraryOptions> _libraryOptions = new Dictionary<string, LibraryOptions>(); + private bool _requiresRefresh; + /// <summary> + /// Initializes a new instance of the <see cref="CollectionFolder"/> class. + /// </summary> public CollectionFolder() { PhysicalLocationsList = Array.Empty<string>(); PhysicalFolderIds = Array.Empty<Guid>(); } - [IgnoreDataMember] + /// <summary> + /// Gets the display preferences id. + /// </summary> + /// <remarks> + /// Allow different display preferences for each collection folder. + /// </remarks> + /// <value>The display prefs id.</value> + [JsonIgnore] + public override Guid DisplayPreferencesId => Id; + + [JsonIgnore] + public override string[] PhysicalLocations => PhysicalLocationsList; + + public string[] PhysicalLocationsList { get; set; } + + public Guid[] PhysicalFolderIds { get; set; } + + public static IXmlSerializer XmlSerializer { get; set; } + + public static IServerApplicationHost ApplicationHost { get; set; } + + [JsonIgnore] public override bool SupportsPlayedStatus => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; + public string CollectionType { get; set; } + + /// <summary> + /// Gets the item's children. + /// </summary> + /// <remarks> + /// Our children are actually just references to the ones in the physical root... + /// </remarks> + /// <value>The actual children.</value> + [JsonIgnore] + public override IEnumerable<BaseItem> Children => GetActualChildren(); + + [JsonIgnore] + public override bool SupportsPeople => false; + public override bool CanDelete() { return false; } - public string CollectionType { get; set; } - - private static readonly Dictionary<string, LibraryOptions> LibraryOptions = new Dictionary<string, LibraryOptions>(); public LibraryOptions GetLibraryOptions() { return GetLibraryOptions(Path); @@ -56,9 +97,7 @@ namespace MediaBrowser.Controller.Entities { try { - var result = XmlSerializer.DeserializeFromFile(typeof(LibraryOptions), GetLibraryOptionsPath(path)) as LibraryOptions; - - if (result == null) + if (XmlSerializer.DeserializeFromFile(typeof(LibraryOptions), GetLibraryOptionsPath(path)) is not LibraryOptions result) { return new LibraryOptions(); } @@ -101,12 +140,12 @@ namespace MediaBrowser.Controller.Entities public static LibraryOptions GetLibraryOptions(string path) { - lock (LibraryOptions) + lock (_libraryOptions) { - if (!LibraryOptions.TryGetValue(path, out var options)) + if (!_libraryOptions.TryGetValue(path, out var options)) { options = LoadLibraryOptions(path); - LibraryOptions[path] = options; + _libraryOptions[path] = options; } return options; @@ -115,11 +154,11 @@ namespace MediaBrowser.Controller.Entities public static void SaveLibraryOptions(string path, LibraryOptions options) { - lock (LibraryOptions) + lock (_libraryOptions) { - LibraryOptions[path] = options; + _libraryOptions[path] = options; - var clone = JsonSerializer.DeserializeFromString<LibraryOptions>(JsonSerializer.SerializeToString(options)); + var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions); foreach (var mediaPath in clone.PathInfos) { if (!string.IsNullOrEmpty(mediaPath.Path)) @@ -134,36 +173,22 @@ namespace MediaBrowser.Controller.Entities public static void OnCollectionFolderChange() { - lock (LibraryOptions) + lock (_libraryOptions) { - LibraryOptions.Clear(); + _libraryOptions.Clear(); } } - /// <summary> - /// Allow different display preferences for each collection folder - /// </summary> - /// <value>The display prefs id.</value> - [IgnoreDataMember] - public override Guid DisplayPreferencesId => Id; - - [IgnoreDataMember] - public override string[] PhysicalLocations => PhysicalLocationsList; - public override bool IsSaveLocalMetadataEnabled() { return true; } - public string[] PhysicalLocationsList { get; set; } - public Guid[] PhysicalFolderIds { get; set; } - protected override FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService) { return CreateResolveArgs(directoryService, true).FileSystemChildren; } - private bool _requiresRefresh; public override bool RequiresRefresh() { var changed = base.RequiresRefresh() || _requiresRefresh; @@ -172,7 +197,7 @@ namespace MediaBrowser.Controller.Entities { var locations = PhysicalLocations; - var newLocations = CreateResolveArgs(new DirectoryService(Logger, FileSystem), false).PhysicalLocations; + var newLocations = CreateResolveArgs(new DirectoryService(FileSystem), false).PhysicalLocations; if (!locations.SequenceEqual(newLocations)) { @@ -195,9 +220,9 @@ namespace MediaBrowser.Controller.Entities return changed; } - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var changed = base.BeforeMetadataRefresh(replaceAllMetdata) || _requiresRefresh; + var changed = base.BeforeMetadataRefresh(replaceAllMetadata) || _requiresRefresh; _requiresRefresh = false; return changed; } @@ -223,7 +248,7 @@ namespace MediaBrowser.Controller.Entities return null; } - return (totalProgresses / foldersWithProgress); + return totalProgresses / foldersWithProgress; } protected override bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren) @@ -266,7 +291,6 @@ namespace MediaBrowser.Controller.Entities var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService) { FileInfo = FileSystem.GetDirectoryInfo(path), - Path = path, Parent = GetParent() as Folder, CollectionType = CollectionType }; @@ -293,27 +317,20 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes - /// ***Currently does not contain logic to maintain items that are unavailable in the file system*** + /// ***Currently does not contain logic to maintain items that are unavailable in the file system***. /// </summary> /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> /// <param name="recursive">if set to <c>true</c> [recursive].</param> /// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param> /// <param name="refreshOptions">The refresh options.</param> /// <param name="directoryService">The directory service.</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { return Task.CompletedTask; } - /// <summary> - /// Our children are actually just references to the ones in the physical root... - /// </summary> - /// <value>The actual children.</value> - [IgnoreDataMember] - public override IEnumerable<BaseItem> Children => GetActualChildren(); - public IEnumerable<BaseItem> GetActualChildren() { return GetPhysicalFolders(true).SelectMany(c => c.Children); @@ -350,9 +367,7 @@ namespace MediaBrowser.Controller.Entities if (result.Count == 0) { - var folder = LibraryManager.FindByPath(path, true) as Folder; - - if (folder != null) + if (LibraryManager.FindByPath(path, true) is Folder folder) { result.Add(folder); } @@ -360,8 +375,5 @@ namespace MediaBrowser.Controller.Entities return result; } - - [IgnoreDataMember] - public override bool SupportsPeople => false; } } diff --git a/MediaBrowser.Controller/Entities/DayOfWeekHelper.cs b/MediaBrowser.Controller/Entities/DayOfWeekHelper.cs deleted file mode 100644 index 8a79e0783..000000000 --- a/MediaBrowser.Controller/Entities/DayOfWeekHelper.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Generic; -using MediaBrowser.Model.Configuration; - -namespace MediaBrowser.Controller.Entities -{ - public static class DayOfWeekHelper - { - public static List<DayOfWeek> GetDaysOfWeek(DynamicDayOfWeek day) - { - return GetDaysOfWeek(new List<DynamicDayOfWeek> { day }); - } - - public static List<DayOfWeek> GetDaysOfWeek(List<DynamicDayOfWeek> days) - { - var list = new List<DayOfWeek>(); - - if (days.Contains(DynamicDayOfWeek.Sunday) || - days.Contains(DynamicDayOfWeek.Weekend) || - days.Contains(DynamicDayOfWeek.Everyday)) - { - list.Add(DayOfWeek.Sunday); - } - - if (days.Contains(DynamicDayOfWeek.Saturday) || - days.Contains(DynamicDayOfWeek.Weekend) || - days.Contains(DynamicDayOfWeek.Everyday)) - { - list.Add(DayOfWeek.Saturday); - } - - if (days.Contains(DynamicDayOfWeek.Monday) || - days.Contains(DynamicDayOfWeek.Weekday) || - days.Contains(DynamicDayOfWeek.Everyday)) - { - list.Add(DayOfWeek.Monday); - } - - if (days.Contains(DynamicDayOfWeek.Tuesday) || - days.Contains(DynamicDayOfWeek.Weekday) || - days.Contains(DynamicDayOfWeek.Everyday)) - { - list.Add(DayOfWeek.Tuesday - ); - } - - if (days.Contains(DynamicDayOfWeek.Wednesday) || - days.Contains(DynamicDayOfWeek.Weekday) || - days.Contains(DynamicDayOfWeek.Everyday)) - { - list.Add(DayOfWeek.Wednesday); - } - - if (days.Contains(DynamicDayOfWeek.Thursday) || - days.Contains(DynamicDayOfWeek.Weekday) || - days.Contains(DynamicDayOfWeek.Everyday)) - { - list.Add(DayOfWeek.Thursday); - } - - if (days.Contains(DynamicDayOfWeek.Friday) || - days.Contains(DynamicDayOfWeek.Weekday) || - days.Contains(DynamicDayOfWeek.Everyday)) - { - list.Add(DayOfWeek.Friday); - } - - return list; - } - } -} diff --git a/MediaBrowser.Controller/Entities/Extensions.cs b/MediaBrowser.Controller/Entities/Extensions.cs index f3bddd29c..9ce8eebe3 100644 --- a/MediaBrowser.Controller/Entities/Extensions.cs +++ b/MediaBrowser.Controller/Entities/Extensions.cs @@ -1,17 +1,22 @@ +#nullable disable + using System; using System.Linq; +using Jellyfin.Extensions; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Entities { /// <summary> - /// Class Extensions + /// Class Extensions. /// </summary> public static class Extensions { /// <summary> /// Adds the trailer URL. /// </summary> + /// <param name="item">Media item.</param> + /// <param name="url">Trailer URL.</param> public static void AddTrailerUrl(this BaseItem item, string url) { if (string.IsNullOrEmpty(url)) @@ -28,13 +33,17 @@ namespace MediaBrowser.Controller.Entities Url = url }; - if (item.RemoteTrailers.Length == 0) + if (item.RemoteTrailers.Count == 0) { item.RemoteTrailers = new[] { mediaUrl }; } else { - item.RemoteTrailers = item.RemoteTrailers.Concat(new[] { mediaUrl }).ToArray(); + var oldIds = item.RemoteTrailers; + var newIds = new MediaUrl[oldIds.Count + 1]; + oldIds.CopyTo(newIds); + newIds[oldIds.Count] = mediaUrl; + item.RemoteTrailers = newIds; } } } diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 8bfadbee6..18b4ec3c6 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1,9 +1,17 @@ +#nullable disable + +#pragma warning disable CA1002, CA1721, CA1819, CS1591 + using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Collections; @@ -11,23 +19,29 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; +using Episode = MediaBrowser.Controller.Entities.TV.Episode; +using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; +using Season = MediaBrowser.Controller.Entities.TV.Season; +using Series = MediaBrowser.Controller.Entities.TV.Series; namespace MediaBrowser.Controller.Entities { /// <summary> - /// Class Folder + /// Class Folder. /// </summary> public class Folder : BaseItem { - public static IUserManager UserManager { get; set; } + public Folder() + { + LinkedChildren = Array.Empty<LinkedChild>(); + } + public static IUserViewManager UserViewManager { get; set; } /// <summary> @@ -38,45 +52,121 @@ namespace MediaBrowser.Controller.Entities public LinkedChild[] LinkedChildren { get; set; } - [IgnoreDataMember] + [JsonIgnore] public DateTime? DateLastMediaAdded { get; set; } - public Folder() - { - LinkedChildren = EmptyLinkedChildArray; - } - - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsThemeMedia => true; - [IgnoreDataMember] + [JsonIgnore] public virtual bool IsPreSorted => false; - [IgnoreDataMember] + [JsonIgnore] public virtual bool IsPhysicalRoot => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => true; /// <summary> /// Gets a value indicating whether this instance is folder. /// </summary> /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public override bool IsFolder => true; - [IgnoreDataMember] + [JsonIgnore] public override bool IsDisplayedAsFolder => true; - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsCumulativeRunTimeTicks => false; - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsDateLastMediaAdded => false; + [JsonIgnore] + public override string FileNameWithoutExtension + { + get + { + if (IsFileProtocol) + { + return System.IO.Path.GetFileName(Path); + } + + return null; + } + } + + /// <summary> + /// Gets the actual children. + /// </summary> + /// <value>The actual children.</value> + [JsonIgnore] + public virtual IEnumerable<BaseItem> Children => LoadChildren(); + + /// <summary> + /// Gets thread-safe access to all recursive children of this folder - without regard to user. + /// </summary> + /// <value>The recursive children.</value> + [JsonIgnore] + public IEnumerable<BaseItem> RecursiveChildren => GetRecursiveChildren(); + + [JsonIgnore] + protected virtual bool SupportsShortcutChildren => false; + + protected virtual bool FilterLinkedChildrenPerUser => false; + + [JsonIgnore] + protected override bool SupportsOwnedItems => base.SupportsOwnedItems || SupportsShortcutChildren; + + [JsonIgnore] + public virtual bool SupportsUserDataFromChildren + { + get + { + // These are just far too slow. + if (this is ICollectionFolder) + { + return false; + } + + if (this is UserView) + { + return false; + } + + if (this is UserRootFolder) + { + return false; + } + + if (this is Channel) + { + return false; + } + + if (SourceType != SourceType.Library) + { + return false; + } + + if (this is IItemByName) + { + if (this is not IHasDualAccess hasDualAccess || hasDualAccess.IsAccessedByName) + { + return false; + } + } + + return true; + } + } + + public static ICollectionManager CollectionManager { get; set; } + public override bool CanDelete() { if (IsRoot) @@ -99,44 +189,27 @@ namespace MediaBrowser.Controller.Entities return baseResult; } - [IgnoreDataMember] - public override string FileNameWithoutExtension - { - get - { - if (IsFileProtocol) - { - return System.IO.Path.GetFileName(Path); - } - - return null; - } - } - protected override bool IsAllowTagFilterEnforced() { if (this is ICollectionFolder) { return false; } + if (this is UserView) { return false; } + return true; } - [IgnoreDataMember] - protected virtual bool SupportsShortcutChildren => false; - /// <summary> /// Adds the child. /// </summary> /// <param name="item">The item.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="InvalidOperationException">Unable to add + item.Name</exception> - public void AddChild(BaseItem item, CancellationToken cancellationToken) + /// <exception cref="InvalidOperationException">Unable to add + item.Name.</exception> + public void AddChild(BaseItem item) { item.SetParent(this); @@ -149,6 +222,7 @@ namespace MediaBrowser.Controller.Entities { item.DateCreated = DateTime.UtcNow; } + if (item.DateModified == DateTime.MinValue) { item.DateModified = DateTime.UtcNow; @@ -157,37 +231,22 @@ namespace MediaBrowser.Controller.Entities LibraryManager.CreateItem(item, this); } - /// <summary> - /// Gets the actual children. - /// </summary> - /// <value>The actual children.</value> - [IgnoreDataMember] - public virtual IEnumerable<BaseItem> Children => LoadChildren(); - - /// <summary> - /// thread-safe access to all recursive children of this folder - without regard to user - /// </summary> - /// <value>The recursive children.</value> - [IgnoreDataMember] - public IEnumerable<BaseItem> RecursiveChildren => GetRecursiveChildren(); - public override bool IsVisible(User user) { - if (this is ICollectionFolder && !(this is BasePluginFolder)) + if (this is ICollectionFolder && this is not BasePluginFolder) { - if (user.Policy.BlockedMediaFolders != null) + var blockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders); + if (blockedMediaFolders.Length > 0) { - if (user.Policy.BlockedMediaFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase) || - - // Backwards compatibility - user.Policy.BlockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase)) + if (blockedMediaFolders.Contains(Id)) { return false; } } else { - if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) + if (!user.HasPermission(PermissionKind.EnableAllFolders) + && !user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(Id)) { return false; } @@ -199,12 +258,13 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Loads our children. Validation will occur externally. - /// We want this sychronous. + /// We want this synchronous. /// </summary> + /// <returns>Returns children.</returns> protected virtual List<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 + // 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 return GetCachedChildren(); } @@ -215,20 +275,20 @@ namespace MediaBrowser.Controller.Entities public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken) { - return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem))); + return ValidateChildren(progress, new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken: cancellationToken); } /// <summary> - /// Validates that the children of the folder still exist + /// Validates that the children of the folder still exist. /// </summary> /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> /// <param name="metadataRefreshOptions">The metadata refresh options.</param> /// <param name="recursive">if set to <c>true</c> [recursive].</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true) + public Task ValidateChildren(IProgress<double> progress, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true, CancellationToken cancellationToken = default) { - return ValidateChildrenInternal(progress, cancellationToken, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService); + return ValidateChildrenInternal(progress, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService, cancellationToken); } private Dictionary<Guid, BaseItem> GetActualChildrenDictionary() @@ -242,7 +302,8 @@ namespace MediaBrowser.Controller.Entities var id = child.Id; if (dictionary.ContainsKey(id)) { - Logger.LogError("Found folder containing items with duplicate id. Path: {path}, Child Name: {ChildName}", + Logger.LogError( + "Found folder containing items with duplicate id. Path: {path}, Child Name: {ChildName}", Path ?? Name, child.Path ?? child.Name); } @@ -267,13 +328,13 @@ namespace MediaBrowser.Controller.Entities /// Validates the children internal. /// </summary> /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> /// <param name="recursive">if set to <c>true</c> [recursive].</param> /// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param> /// <param name="refreshOptions">The refresh options.</param> /// <param name="directoryService">The directory service.</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - protected virtual async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + protected virtual async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { if (recursive) { @@ -282,7 +343,7 @@ namespace MediaBrowser.Controller.Entities try { - await ValidateChildrenInternal2(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService).ConfigureAwait(false); + await ValidateChildrenInternal2(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken).ConfigureAwait(false); } finally { @@ -293,7 +354,7 @@ namespace MediaBrowser.Controller.Entities } } - private async Task ValidateChildrenInternal2(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + private async Task ValidateChildrenInternal2(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -314,17 +375,17 @@ namespace MediaBrowser.Controller.Entities return; } - progress.Report(5); + progress.Report(ProgressHelpers.RetrievedChildren); if (recursive) { - ProviderManager.OnRefreshProgress(this, 5); + ProviderManager.OnRefreshProgress(this, ProgressHelpers.RetrievedChildren); } - //build a dictionary of the current children we have now by Id so we can compare quickly and easily + // Build a dictionary of the current children we have now by Id so we can compare quickly and easily var currentChildren = GetActualChildrenDictionary(); - //create a list for our validated children + // Create a list for our validated children var newItems = new List<BaseItem>(); cancellationToken.ThrowIfCancellationRequested(); @@ -337,7 +398,12 @@ namespace MediaBrowser.Controller.Entities if (currentChild.UpdateFromResolvedItem(child) > ItemUpdateType.None) { - currentChild.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken); + await currentChild.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); + } + else + { + // metadata is up-to-date; make sure DB has correct images dimensions and hash + await LibraryManager.UpdateImagesAsync(currentChild).ConfigureAwait(false); } continue; @@ -374,11 +440,11 @@ namespace MediaBrowser.Controller.Entities validChildrenNeedGeneration = true; } - progress.Report(10); + progress.Report(ProgressHelpers.UpdatedChildItems); if (recursive) { - ProviderManager.OnRefreshProgress(this, 10); + ProviderManager.OnRefreshProgress(this, ProgressHelpers.UpdatedChildItems); } cancellationToken.ThrowIfCancellationRequested(); @@ -388,11 +454,13 @@ namespace MediaBrowser.Controller.Entities var innerProgress = new ActionableProgress<double>(); var folder = this; - innerProgress.RegisterAction(p => + innerProgress.RegisterAction(innerPercent => { - double newPct = .80 * p + 10; - progress.Report(newPct); - ProviderManager.OnRefreshProgress(folder, newPct); + var percent = ProgressHelpers.GetProgress(ProgressHelpers.UpdatedChildItems, ProgressHelpers.ScannedSubfolders, innerPercent); + + progress.Report(percent); + + ProviderManager.OnRefreshProgress(folder, percent); }); if (validChildrenNeedGeneration) @@ -406,11 +474,11 @@ namespace MediaBrowser.Controller.Entities if (refreshChildMetadata) { - progress.Report(90); + progress.Report(ProgressHelpers.ScannedSubfolders); if (recursive) { - ProviderManager.OnRefreshProgress(this, 90); + ProviderManager.OnRefreshProgress(this, ProgressHelpers.ScannedSubfolders); } var container = this as IMetadataContainer; @@ -418,13 +486,15 @@ namespace MediaBrowser.Controller.Entities var innerProgress = new ActionableProgress<double>(); var folder = this; - innerProgress.RegisterAction(p => + innerProgress.RegisterAction(innerPercent => { - double newPct = .10 * p + 90; - progress.Report(newPct); + var percent = ProgressHelpers.GetProgress(ProgressHelpers.ScannedSubfolders, ProgressHelpers.RefreshedMetadata, innerPercent); + + progress.Report(percent); + if (recursive) { - ProviderManager.OnRefreshProgress(folder, newPct); + ProviderManager.OnRefreshProgress(folder, percent); } }); @@ -439,55 +509,35 @@ namespace MediaBrowser.Controller.Entities validChildren = Children.ToList(); } - await RefreshMetadataRecursive(validChildren, refreshOptions, recursive, innerProgress, cancellationToken); + await RefreshMetadataRecursive(validChildren, refreshOptions, recursive, innerProgress, cancellationToken).ConfigureAwait(false); } } } - private async Task RefreshMetadataRecursive(List<BaseItem> children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken) + private Task RefreshMetadataRecursive(IList<BaseItem> children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken) { - var numComplete = 0; - var count = children.Count; - double currentPercent = 0; - - foreach (var child in children) - { - cancellationToken.ThrowIfCancellationRequested(); - - var innerProgress = new ActionableProgress<double>(); - - // Avoid implicitly captured closure - var currentInnerPercent = currentPercent; - - innerProgress.RegisterAction(p => - { - double innerPercent = currentInnerPercent; - innerPercent += p / (count); - progress.Report(innerPercent); - }); - - await RefreshChildMetadata(child, refreshOptions, recursive && child.IsFolder, innerProgress, cancellationToken) - .ConfigureAwait(false); - - numComplete++; - double percent = numComplete; - percent /= count; - percent *= 100; - currentPercent = percent; - - progress.Report(percent); - } + return RunTasks( + (baseItem, innerProgress) => RefreshChildMetadata(baseItem, refreshOptions, recursive && baseItem.IsFolder, innerProgress, cancellationToken), + children, + progress, + cancellationToken); } private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken) { - var series = container as Series; - if (series != null) - { - await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + // limit the amount of concurrent metadata refreshes + await ProviderManager.RunMetadataRefresh( + async () => + { + var series = container as Series; + if (series != null) + { + await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + } - } - await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false); + await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false); + }, + cancellationToken).ConfigureAwait(false); } private async Task RefreshChildMetadata(BaseItem child, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken) @@ -502,12 +552,15 @@ namespace MediaBrowser.Controller.Entities { if (refreshOptions.RefreshItem(child)) { - await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + // limit the amount of concurrent metadata refreshes + await ProviderManager.RunMetadataRefresh( + async () => await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false), + cancellationToken).ConfigureAwait(false); } if (recursive && child is Folder folder) { - await folder.RefreshMetadataRecursive(folder.Children.ToList(), refreshOptions, true, progress, cancellationToken); + await folder.RefreshMetadataRecursive(folder.Children.ToList(), refreshOptions, true, progress, cancellationToken).ConfigureAwait(false); } } } @@ -520,45 +573,80 @@ namespace MediaBrowser.Controller.Entities /// <param name="progress">The progress.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - private async Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<double> progress, CancellationToken cancellationToken) + private Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<double> progress, CancellationToken cancellationToken) { - var numComplete = 0; - var count = children.Count; - double currentPercent = 0; + return RunTasks( + (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, true, false, null, directoryService, cancellationToken), + children, + progress, + cancellationToken); + } - foreach (var child in children) + /// <summary> + /// Runs an action block on a list of children. + /// </summary> + /// <param name="task">The task to run for each child.</param> + /// <param name="children">The list of children.</param> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + private async Task RunTasks<T>(Func<T, IProgress<double>, Task> task, IList<T> children, IProgress<double> progress, CancellationToken cancellationToken) + { + var childrenCount = children.Count; + var childrenProgress = new double[childrenCount]; + + void UpdateProgress() { - cancellationToken.ThrowIfCancellationRequested(); + progress.Report(childrenProgress.Average()); + } - var innerProgress = new ActionableProgress<double>(); + var fanoutConcurrency = ConfigurationManager.Configuration.LibraryScanFanoutConcurrency; + var parallelism = fanoutConcurrency == 0 ? Environment.ProcessorCount : fanoutConcurrency; + + var actionBlock = new ActionBlock<int>( + async i => + { + var innerProgress = new ActionableProgress<double>(); + + innerProgress.RegisterAction(innerPercent => + { + // round the percent and only update progress if it changed to prevent excessive UpdateProgress calls + var innerPercentRounded = Math.Round(innerPercent); + if (childrenProgress[i] != innerPercentRounded) + { + childrenProgress[i] = innerPercentRounded; + UpdateProgress(); + } + }); + + await task(children[i], innerProgress).ConfigureAwait(false); - // Avoid implicitly captured closure - var currentInnerPercent = currentPercent; + childrenProgress[i] = 100; - innerProgress.RegisterAction(p => + UpdateProgress(); + }, + new ExecutionDataflowBlockOptions { - double innerPercent = currentInnerPercent; - innerPercent += p / (count); - progress.Report(innerPercent); + MaxDegreeOfParallelism = parallelism, + CancellationToken = cancellationToken, }); - await child.ValidateChildrenInternal(innerProgress, cancellationToken, true, false, null, directoryService) - .ConfigureAwait(false); + for (var i = 0; i < childrenCount; i++) + { + actionBlock.Post(i); + } - numComplete++; - double percent = numComplete; - percent /= count; - percent *= 100; - currentPercent = percent; + actionBlock.Complete(); - progress.Report(percent); - } + await actionBlock.Completion.ConfigureAwait(false); } /// <summary> - /// Get the children of this folder from the actual file system + /// Get the children of this folder from the actual file system. /// </summary> /// <returns>IEnumerable{BaseItem}.</returns> + /// <param name="directoryService">The directory service to use for operation.</param> + /// <returns>Returns set of base items.</returns> protected virtual IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService) { var collectionType = LibraryManager.GetContentType(this); @@ -568,7 +656,7 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Get our children from the repo - stubbed for now + /// Get our children from the repo - stubbed for now. /// </summary> /// <returns>IEnumerable{BaseItem}.</returns> protected List<BaseItem> GetCachedChildren() @@ -585,7 +673,7 @@ namespace MediaBrowser.Controller.Entities { if (LinkedChildren.Length > 0) { - if (!(this is ICollectionFolder)) + if (this is not ICollectionFolder) { return GetChildren(user, true).Count; } @@ -600,7 +688,6 @@ namespace MediaBrowser.Controller.Entities { EnableImages = false } - }); return result.TotalRecordCount; @@ -619,7 +706,6 @@ namespace MediaBrowser.Controller.Entities { EnableImages = false } - }).TotalRecordCount; } @@ -644,7 +730,7 @@ namespace MediaBrowser.Controller.Entities return PostFilterAndSort(items, query, true); } - if (!(this is UserRootFolder) && !(this is AggregateFolder) && query.ParentId == Guid.Empty) + if (this is not UserRootFolder && this is not AggregateFolder && query.ParentId == Guid.Empty) { query.Parent = this; } @@ -665,36 +751,36 @@ namespace MediaBrowser.Controller.Entities query.StartIndex = null; query.Limit = null; - var itemsList = LibraryManager.GetItemList(query); + IEnumerable<BaseItem> itemsList = LibraryManager.GetItemList(query); var user = query.User; if (user != null) { // needed for boxsets - itemsList = itemsList.Where(i => i.IsVisibleStandalone(query.User)).ToList(); + itemsList = itemsList.Where(i => i.IsVisibleStandalone(query.User)); } - BaseItem[] returnItems; + IEnumerable<BaseItem> returnItems; int totalCount = 0; if (query.EnableTotalRecordCount) { - var itemsArray = itemsList.ToArray(); - totalCount = itemsArray.Length; - returnItems = itemsArray; + var itemArray = itemsList.ToArray(); + totalCount = itemArray.Length; + returnItems = itemArray; } else { - returnItems = itemsList.ToArray(); + returnItems = itemsList; } if (limit.HasValue) { - returnItems = returnItems.Skip(startIndex ?? 0).Take(limit.Value).ToArray(); + returnItems = returnItems.Skip(startIndex ?? 0).Take(limit.Value); } else if (startIndex.HasValue) { - returnItems = returnItems.Skip(startIndex.Value).ToArray(); + returnItems = returnItems.Skip(startIndex.Value); } return new QueryResult<BaseItem> @@ -706,7 +792,7 @@ namespace MediaBrowser.Controller.Entities private bool RequiresPostFiltering2(InternalItemsQuery query) { - if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], typeof(BoxSet).Name, StringComparison.OrdinalIgnoreCase)) + if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase)) { Logger.LogDebug("Query requires post-filtering due to BoxSet query"); return true; @@ -719,7 +805,7 @@ namespace MediaBrowser.Controller.Entities { if (LinkedChildren.Length > 0) { - if (!(this is ICollectionFolder)) + if (this is not ICollectionFolder) { Logger.LogDebug("Query requires post-filtering due to LinkedChildren. Type: " + GetType().Name); return true; @@ -796,7 +882,7 @@ namespace MediaBrowser.Controller.Entities if (query.IsPlayed.HasValue) { - if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(typeof(Series).Name)) + if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(nameof(Series))) { Logger.LogDebug("Query requires post-filtering due to IsPlayed"); return true; @@ -806,26 +892,51 @@ namespace MediaBrowser.Controller.Entities return false; } + private static BaseItem[] SortItemsByRequest(InternalItemsQuery query, IReadOnlyList<BaseItem> items) + { + var ids = query.ItemIds; + int size = items.Count; + + // ids can potentially contain non-unique guids, but query result cannot, + // so we include only first occurrence of each guid + var positions = new Dictionary<Guid, int>(size); + int index = 0; + for (int i = 0; i < ids.Length; i++) + { + if (positions.TryAdd(ids[i], index)) + { + index++; + } + } + + var newItems = new BaseItem[size]; + for (int i = 0; i < size; i++) + { + var item = items[i]; + newItems[positions[item.Id]] = item; + } + + return newItems; + } + public QueryResult<BaseItem> GetItems(InternalItemsQuery query) { if (query.ItemIds.Length > 0) { var result = LibraryManager.GetItemsResult(query); - if (query.OrderBy.Length == 0) + if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1) { - var ids = query.ItemIds.ToList(); - - // Try to preserve order - result.Items = result.Items.OrderBy(i => ids.IndexOf(i.Id)).ToArray(); + result.Items = SortItemsByRequest(query, result.Items); } + return result; } return GetItemsInternal(query); } - public BaseItem[] GetItemList(InternalItemsQuery query) + public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query) { query.EnableTotalRecordCount = false; @@ -833,14 +944,12 @@ namespace MediaBrowser.Controller.Entities { var result = LibraryManager.GetItemList(query); - if (query.OrderBy.Length == 0) + if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1) { - var ids = query.ItemIds.ToList(); - - // Try to preserve order - return result.OrderBy(i => ids.IndexOf(i.Id)).ToArray(); + return SortItemsByRequest(query, result); } - return result.ToArray(); + + return result; } return GetItemsInternal(query).Items; @@ -853,7 +962,7 @@ namespace MediaBrowser.Controller.Entities try { query.Parent = this; - query.ChannelIds = new Guid[] { ChannelId }; + query.ChannelIds = new[] { ChannelId }; // Don't blow up here because it could cause parent screens with other content to fail return ChannelManager.GetChannelItemsInternal(query, new SimpleProgress<double>(), CancellationToken.None).Result; @@ -882,14 +991,18 @@ namespace MediaBrowser.Controller.Entities } else { - items = GetChildren(user, true).Where(filter); + // need to pass this param to the children. + var childQuery = new InternalItemsQuery + { + DisplayAlbumFolders = query.DisplayAlbumFolders + }; + + items = GetChildren(user, true, childQuery).Where(filter); } return PostFilterAndSort(items, query, true); } - public static ICollectionManager CollectionManager { get; set; } - protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query, bool enableSorting) { var user = query.User; @@ -902,16 +1015,17 @@ namespace MediaBrowser.Controller.Entities if (!string.IsNullOrEmpty(query.NameStartsWithOrGreater)) { - items = items.Where(i => string.Compare(query.NameStartsWithOrGreater, i.SortName, StringComparison.CurrentCultureIgnoreCase) < 1); + items = items.Where(i => string.Compare(query.NameStartsWithOrGreater, i.SortName, StringComparison.InvariantCultureIgnoreCase) < 1); } + if (!string.IsNullOrEmpty(query.NameStartsWith)) { - items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.OrdinalIgnoreCase)); + items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.InvariantCultureIgnoreCase)); } if (!string.IsNullOrEmpty(query.NameLessThan)) { - items = items.Where(i => string.Compare(query.NameLessThan, i.SortName, StringComparison.CurrentCultureIgnoreCase) == 1); + items = items.Where(i => string.Compare(query.NameLessThan, i.SortName, StringComparison.InvariantCultureIgnoreCase) == 1); } // This must be the last filter @@ -923,11 +1037,13 @@ namespace MediaBrowser.Controller.Entities return UserViewBuilder.SortAndPage(items, null, query, LibraryManager, enableSorting); } - private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(IEnumerable<BaseItem> items, + private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded( + IEnumerable<BaseItem> items, InternalItemsQuery query, BaseItem queryParent, User user, - IServerConfigurationManager configurationManager, ICollectionManager collectionManager) + IServerConfigurationManager configurationManager, + ICollectionManager collectionManager) { if (items == null) { @@ -942,7 +1058,8 @@ namespace MediaBrowser.Controller.Entities return items; } - private static bool CollapseBoxSetItems(InternalItemsQuery query, + private static bool CollapseBoxSetItems( + InternalItemsQuery query, BaseItem queryParent, User user, IServerConfigurationManager configurationManager) @@ -952,18 +1069,22 @@ namespace MediaBrowser.Controller.Entities { return false; } + if (queryParent is Series) { return false; } + if (queryParent is Season) { return false; } + if (queryParent is MusicAlbum) { return false; } + if (queryParent is MusicArtist) { return false; @@ -993,33 +1114,38 @@ namespace MediaBrowser.Controller.Entities { return false; } + if (request.IsFavoriteOrLiked.HasValue) { return false; } + if (request.IsLiked.HasValue) { return false; } + if (request.IsPlayed.HasValue) { return false; } + if (request.IsResumable.HasValue) { return false; } + if (request.IsFolder.HasValue) { return false; } - if (request.Genres.Length > 0) + if (request.Genres.Count > 0) { return false; } - if (request.GenreIds.Length > 0) + if (request.GenreIds.Count > 0) { return false; } @@ -1124,7 +1250,7 @@ namespace MediaBrowser.Controller.Entities return false; } - if (request.GenreIds.Length > 0) + if (request.GenreIds.Count > 0) { return false; } @@ -1169,6 +1295,11 @@ namespace MediaBrowser.Controller.Entities public List<BaseItem> GetChildren(User user, bool includeLinkedChildren) { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return GetChildren(user, includeLinkedChildren, null); } @@ -1179,8 +1310,11 @@ namespace MediaBrowser.Controller.Entities throw new ArgumentNullException(nameof(user)); } - //the true root should return our users root folder children - if (IsPhysicalRoot) return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren); + // the true root should return our users root folder children + if (IsPhysicalRoot) + { + return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren); + } var result = new Dictionary<Guid, BaseItem>(); @@ -1197,10 +1331,23 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Adds the children to list. /// </summary> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query) { - foreach (var child in GetEligibleChildrenForRecursiveChildren(user)) + // If Query.AlbumFolders is set, then enforce the format as per the db in that it permits sub-folders in music albums. + IEnumerable<BaseItem> children = null; + if ((query?.DisplayAlbumFolders ?? false) && (this is MusicAlbum)) + { + children = Children; + query = null; + } + + // If there are not sub-folders, proceed as normal. + if (children == null) + { + children = GetEligibleChildrenForRecursiveChildren(user); + } + + foreach (var child in children) { bool? isVisibleToUser = null; @@ -1240,18 +1387,6 @@ namespace MediaBrowser.Controller.Entities } } - /// <summary> - /// Gets allowed recursive children of an item - /// </summary> - /// <param name="user">The user.</param> - /// <param name="includeLinkedChildren">if set to <c>true</c> [include linked children].</param> - /// <returns>IEnumerable{BaseItem}.</returns> - /// <exception cref="ArgumentNullException"></exception> - public IEnumerable<BaseItem> GetRecursiveChildren(User user, bool includeLinkedChildren = true) - { - return GetRecursiveChildren(user, null); - } - public virtual IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) { if (user == null) @@ -1327,7 +1462,6 @@ namespace MediaBrowser.Controller.Entities } } - /// <summary> /// Gets the linked children. /// </summary> @@ -1346,19 +1480,23 @@ namespace MediaBrowser.Controller.Entities list.Add(child); } } + return list; } - protected virtual bool FilterLinkedChildrenPerUser => false; - public bool ContainsLinkedChildByItemId(Guid itemId) { var linkedChildren = LinkedChildren; foreach (var i in linkedChildren) { - if (i.ItemId.HasValue && i.ItemId.Value == itemId) + if (i.ItemId.HasValue) { - return true; + if (i.ItemId.Value == itemId) + { + return true; + } + + continue; } var child = GetLinkedChild(i); @@ -1368,6 +1506,7 @@ namespace MediaBrowser.Controller.Entities return true; } } + return false; } @@ -1406,7 +1545,7 @@ namespace MediaBrowser.Controller.Entities var childOwner = child.GetOwner() ?? child; - if (childOwner != null && !(child is IItemByName)) + if (child is not IItemByName) { var childProtocol = childOwner.PathProtocol; if (!childProtocol.HasValue || childProtocol.Value != Model.MediaInfo.MediaProtocol.File) @@ -1445,9 +1584,6 @@ namespace MediaBrowser.Controller.Entities .Where(i => i.Item2 != null); } - [IgnoreDataMember] - protected override bool SupportsOwnedItems => base.SupportsOwnedItems || SupportsShortcutChildren; - protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) { var changesFound = false; @@ -1468,7 +1604,8 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Refreshes the linked children. /// </summary> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> + /// <param name="fileSystemChildren">The enumerable of file system metadata.</param> + /// <returns><c>true</c> if the linked children were updated, <c>false</c> otherwise.</returns> protected virtual bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren) { if (SupportsShortcutChildren) @@ -1532,8 +1669,8 @@ namespace MediaBrowser.Controller.Entities /// <param name="user">The user.</param> /// <param name="datePlayed">The date played.</param> /// <param name="resetPosition">if set to <c>true</c> [reset position].</param> - /// <returns>Task.</returns> - public override void MarkPlayed(User user, + public override void MarkPlayed( + User user, DateTime? datePlayed, bool resetPosition) { @@ -1545,7 +1682,7 @@ namespace MediaBrowser.Controller.Entities EnableTotalRecordCount = false }; - if (!user.Configuration.DisplayMissingEpisodes) + if (!user.DisplayMissingEpisodes) { query.IsVirtualItem = false; } @@ -1573,7 +1710,6 @@ namespace MediaBrowser.Controller.Entities /// Marks the unplayed. /// </summary> /// <param name="user">The user.</param> - /// <returns>Task.</returns> public override void MarkUnplayed(User user) { var itemsResult = GetItemList(new InternalItemsQuery @@ -1582,7 +1718,6 @@ namespace MediaBrowser.Controller.Entities Recursive = true, IsFolder = false, EnableTotalRecordCount = false - }); // Sweep through recursively and update status @@ -1600,7 +1735,6 @@ namespace MediaBrowser.Controller.Entities IsFolder = false, IsVirtualItem = false, EnableTotalRecordCount = false - }); return itemsResult @@ -1612,46 +1746,6 @@ namespace MediaBrowser.Controller.Entities return !IsPlayed(user); } - [IgnoreDataMember] - public virtual bool SupportsUserDataFromChildren - { - get - { - // These are just far too slow. - if (this is ICollectionFolder) - { - return false; - } - if (this is UserView) - { - return false; - } - if (this is UserRootFolder) - { - return false; - } - if (this is Channel) - { - return false; - } - if (SourceType != SourceType.Library) - { - return false; - } - var iItemByName = this as IItemByName; - if (iItemByName != null) - { - var hasDualAccess = this as IHasDualAccess; - if (hasDualAccess == null || hasDualAccess.IsAccessedByName) - { - return false; - } - } - - return true; - } - } - public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields) { if (!SupportsUserDataFromChildren) @@ -1681,21 +1775,15 @@ namespace MediaBrowser.Controller.Entities { EnableImages = false } + }).TotalRecordCount; - }); - - double unplayedCount = unplayedQueryResult.TotalRecordCount; - - dto.UnplayedItemCount = unplayedQueryResult.TotalRecordCount; + dto.UnplayedItemCount = unplayedQueryResult; - if (itemDto != null && itemDto.RecursiveItemCount.HasValue) + if (itemDto?.RecursiveItemCount > 0) { - if (itemDto.RecursiveItemCount.Value > 0) - { - var unplayedPercentage = (unplayedCount / itemDto.RecursiveItemCount.Value) * 100; - dto.PlayedPercentage = 100 - unplayedPercentage; - dto.Played = dto.PlayedPercentage.Value >= 100; - } + var unplayedPercentage = ((double)unplayedQueryResult / itemDto.RecursiveItemCount.Value) * 100; + dto.PlayedPercentage = 100 - unplayedPercentage; + dto.Played = dto.PlayedPercentage.Value >= 100; } else { @@ -1703,5 +1791,45 @@ namespace MediaBrowser.Controller.Entities } } } + + /// <summary> + /// Contains constants used when reporting scan progress. + /// </summary> + private static class ProgressHelpers + { + /// <summary> + /// Reported after the folders immediate children are retrieved. + /// </summary> + public const int RetrievedChildren = 5; + + /// <summary> + /// Reported after add, updating, or deleting child items from the LibraryManager. + /// </summary> + public const int UpdatedChildItems = 10; + + /// <summary> + /// Reported once subfolders are scanned. + /// When scanning subfolders, the progress will be between [UpdatedItems, ScannedSubfolders]. + /// </summary> + public const int ScannedSubfolders = 50; + + /// <summary> + /// Reported once metadata is refreshed. + /// When refreshing metadata, the progress will be between [ScannedSubfolders, MetadataRefreshed]. + /// </summary> + public const int RefreshedMetadata = 100; + + /// <summary> + /// Gets the current progress given the previous step, next step, and progress in between. + /// </summary> + /// <param name="previousProgressStep">The previous progress step.</param> + /// <param name="nextProgressStep">The next progress step.</param> + /// <param name="currentProgress">The current progress step.</param> + /// <returns>The progress.</returns> + public static double GetProgress(int previousProgressStep, int nextProgressStep, double currentProgress) + { + return previousProgressStep + ((nextProgressStep - previousProgressStep) * (currentProgress / 100)); + } + } } } diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs index 44cb62d22..338f96204 100644 --- a/MediaBrowser.Controller/Entities/Genre.cs +++ b/MediaBrowser.Controller/Entities/Genre.cs @@ -1,17 +1,38 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; +using System.Text.Json.Serialization; +using Diacritics.Extensions; using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Extensions; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities { /// <summary> - /// Class Genre + /// Class Genre. /// </summary> public class Genre : BaseItem, IItemByName { + /// <summary> + /// Gets the folder containing the item. + /// If the item is a folder, it returns the folder itself. + /// </summary> + /// <value>The containing folder path.</value> + [JsonIgnore] + public override string ContainingFolderPath => Path; + + [JsonIgnore] + public override bool IsDisplayedAsFolder => true; + + [JsonIgnore] + public override bool SupportsAncestors => false; + + [JsonIgnore] + public override bool SupportsPeople => false; + public override List<string> GetUserDataKeys() { var list = base.GetUserDataKeys(); @@ -19,6 +40,7 @@ namespace MediaBrowser.Controller.Entities list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics()); return list; } + public override string CreatePresentationUniqueKey() { return GetUserDataKeys()[0]; @@ -29,20 +51,6 @@ namespace MediaBrowser.Controller.Entities return 1; } - /// <summary> - /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself - /// </summary> - /// <value>The containing folder path.</value> - [IgnoreDataMember] - public override string ContainingFolderPath => Path; - - [IgnoreDataMember] - public override bool IsDisplayedAsFolder => true; - - [IgnoreDataMember] - public override bool SupportsAncestors => false; - public override bool IsSaveLocalMetadataEnabled() { return true; @@ -56,14 +64,17 @@ namespace MediaBrowser.Controller.Entities public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) { query.GenreIds = new[] { Id }; - query.ExcludeItemTypes = new[] { typeof(MusicVideo).Name, typeof(Audio.Audio).Name, typeof(MusicAlbum).Name, typeof(MusicArtist).Name }; + query.ExcludeItemTypes = new[] + { + nameof(MusicVideo), + nameof(Entities.Audio.Audio), + nameof(MusicAlbum), + nameof(MusicArtist) + }; return LibraryManager.GetItemList(query); } - [IgnoreDataMember] - public override bool SupportsPeople => false; - public static string GetPath(string name) { return GetPath(name, true); @@ -92,15 +103,18 @@ namespace MediaBrowser.Controller.Entities Logger.LogDebug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); return true; } + return base.RequiresRefresh(); } /// <summary> - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true if changes were made. /// </summary> - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + /// <param name="replaceAllMetadata">Whether to replace all metadata.</param> + /// <returns>true if the item has change, else false.</returns> + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); var newPath = GetRebasedPath(); if (!string.Equals(Path, newPath, StringComparison.Ordinal)) diff --git a/MediaBrowser.Controller/Entities/ICollectionFolder.cs b/MediaBrowser.Controller/Entities/ICollectionFolder.cs index a6a9a0783..89e494ebc 100644 --- a/MediaBrowser.Controller/Entities/ICollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/ICollectionFolder.cs @@ -1,15 +1,22 @@ +#nullable disable + +#pragma warning disable CA1819, CS1591 + using System; namespace MediaBrowser.Controller.Entities { /// <summary> - /// This is just a marker interface to denote top level folders + /// This is just a marker interface to denote top level folders. /// </summary> public interface ICollectionFolder : IHasCollectionType { string Path { get; } + string Name { get; } + Guid Id { get; } + string[] PhysicalLocations { get; } } diff --git a/MediaBrowser.Controller/Entities/IHasAspectRatio.cs b/MediaBrowser.Controller/Entities/IHasAspectRatio.cs index 149c1e5ab..3aeb7468f 100644 --- a/MediaBrowser.Controller/Entities/IHasAspectRatio.cs +++ b/MediaBrowser.Controller/Entities/IHasAspectRatio.cs @@ -1,7 +1,9 @@ +#nullable disable + namespace MediaBrowser.Controller.Entities { /// <summary> - /// Interface IHasAspectRatio + /// Interface IHasAspectRatio. /// </summary> public interface IHasAspectRatio { diff --git a/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs index abee75a28..14459624e 100644 --- a/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs +++ b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs @@ -1,7 +1,9 @@ +#nullable disable + namespace MediaBrowser.Controller.Entities { /// <summary> - /// Interface IHasDisplayOrder + /// Interface IHasDisplayOrder. /// </summary> public interface IHasDisplayOrder { diff --git a/MediaBrowser.Controller/Entities/IHasMediaSources.cs b/MediaBrowser.Controller/Entities/IHasMediaSources.cs index 4635b9062..90d9bdd2d 100644 --- a/MediaBrowser.Controller/Entities/IHasMediaSources.cs +++ b/MediaBrowser.Controller/Entities/IHasMediaSources.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.Dto; @@ -7,13 +11,19 @@ namespace MediaBrowser.Controller.Entities { public interface IHasMediaSources { + Guid Id { get; set; } + + long? RunTimeTicks { get; set; } + + string Path { get; } + /// <summary> /// Gets the media sources. /// </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); + List<MediaStream> GetMediaStreams(); - Guid Id { get; set; } - long? RunTimeTicks { get; set; } - string Path { get; } } } diff --git a/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs b/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs index 777b40828..f80f7c304 100644 --- a/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs +++ b/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using MediaBrowser.Model.LiveTv; namespace MediaBrowser.Controller.Entities @@ -5,13 +9,21 @@ namespace MediaBrowser.Controller.Entities public interface IHasProgramAttributes { bool IsMovie { get; set; } + bool IsSports { get; } + bool IsNews { get; } + bool IsKids { get; } + bool IsRepeat { get; set; } + bool IsSeries { get; set; } + ProgramAudio? Audio { get; set; } + string EpisodeTitle { get; set; } + string ServiceName { get; set; } } } diff --git a/MediaBrowser.Controller/Entities/IHasScreenshots.cs b/MediaBrowser.Controller/Entities/IHasScreenshots.cs index 0975242f5..ae01c223e 100644 --- a/MediaBrowser.Controller/Entities/IHasScreenshots.cs +++ b/MediaBrowser.Controller/Entities/IHasScreenshots.cs @@ -1,7 +1,7 @@ namespace MediaBrowser.Controller.Entities { /// <summary> - /// Interface IHasScreenshots + /// The item has screenshots. /// </summary> public interface IHasScreenshots { diff --git a/MediaBrowser.Controller/Entities/IHasSeries.cs b/MediaBrowser.Controller/Entities/IHasSeries.cs index 7da53f730..5f774bbde 100644 --- a/MediaBrowser.Controller/Entities/IHasSeries.cs +++ b/MediaBrowser.Controller/Entities/IHasSeries.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.Entities @@ -5,15 +9,21 @@ namespace MediaBrowser.Controller.Entities public interface IHasSeries { /// <summary> - /// Gets the name of the series. + /// Gets or sets the name of the series. /// </summary> /// <value>The name of the series.</value> string SeriesName { get; set; } + + Guid SeriesId { get; set; } + + string SeriesPresentationUniqueKey { get; set; } + string FindSeriesName(); + string FindSeriesSortName(); - Guid SeriesId { get; set; } + Guid FindSeriesId(); - string SeriesPresentationUniqueKey { get; set; } + string FindSeriesPresentationUniqueKey(); } } diff --git a/MediaBrowser.Controller/Entities/IHasShares.cs b/MediaBrowser.Controller/Entities/IHasShares.cs new file mode 100644 index 000000000..dca5af873 --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasShares.cs @@ -0,0 +1,11 @@ +#nullable disable + +#pragma warning disable CA1819, CS1591 + +namespace MediaBrowser.Controller.Entities +{ + public interface IHasShares + { + Share[] Shares { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs index 688439e6c..f317a02ff 100644 --- a/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs +++ b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs @@ -1,4 +1,9 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; +using System.Collections.Generic; namespace MediaBrowser.Controller.Entities { @@ -8,6 +13,6 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the special feature ids. /// </summary> /// <value>The special feature ids.</value> - Guid[] SpecialFeatureIds { get; set; } + IReadOnlyList<Guid> SpecialFeatureIds { get; set; } } } diff --git a/MediaBrowser.Controller/Entities/IHasStartDate.cs b/MediaBrowser.Controller/Entities/IHasStartDate.cs index 1ecde9af4..dab15eb01 100644 --- a/MediaBrowser.Controller/Entities/IHasStartDate.cs +++ b/MediaBrowser.Controller/Entities/IHasStartDate.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.Entities diff --git a/MediaBrowser.Controller/Entities/IHasTrailers.cs b/MediaBrowser.Controller/Entities/IHasTrailers.cs index 3bdb9b64a..f4271678d 100644 --- a/MediaBrowser.Controller/Entities/IHasTrailers.cs +++ b/MediaBrowser.Controller/Entities/IHasTrailers.cs @@ -1,6 +1,9 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; -using System.Linq; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Entities @@ -11,29 +14,85 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the remote trailers. /// </summary> /// <value>The remote trailers.</value> - MediaUrl[] RemoteTrailers { get; set; } + IReadOnlyList<MediaUrl> RemoteTrailers { get; set; } /// <summary> /// Gets or sets the local trailer ids. /// </summary> /// <value>The local trailer ids.</value> - Guid[] LocalTrailerIds { get; set; } - Guid[] RemoteTrailerIds { get; set; } + IReadOnlyList<Guid> LocalTrailerIds { get; set; } + + /// <summary> + /// Gets or sets the remote trailer ids. + /// </summary> + /// <value>The remote trailer ids.</value> + IReadOnlyList<Guid> RemoteTrailerIds { get; set; } + Guid Id { get; set; } } + /// <summary> + /// Class providing extension methods for working with <see cref="IHasTrailers" />. + /// </summary> public static class HasTrailerExtensions { /// <summary> + /// Gets the trailer count. + /// </summary> + /// <param name="item">Media item.</param> + /// <returns><see cref="IReadOnlyList{Guid}" />.</returns> + public static int GetTrailerCount(this IHasTrailers item) + => item.LocalTrailerIds.Count + item.RemoteTrailerIds.Count; + + /// <summary> /// Gets the trailer ids. /// </summary> - /// <returns>List<Guid>.</returns> - public static List<Guid> GetTrailerIds(this IHasTrailers item) + /// <param name="item">Media item.</param> + /// <returns><see cref="IReadOnlyList{Guid}" />.</returns> + public static IReadOnlyList<Guid> GetTrailerIds(this IHasTrailers item) { - var list = item.LocalTrailerIds.ToList(); - list.AddRange(item.RemoteTrailerIds); - return list; + var localIds = item.LocalTrailerIds; + var remoteIds = item.RemoteTrailerIds; + + var all = new Guid[localIds.Count + remoteIds.Count]; + var index = 0; + foreach (var id in localIds) + { + all[index++] = id; + } + + foreach (var id in remoteIds) + { + all[index++] = id; + } + + return all; } + /// <summary> + /// Gets the trailers. + /// </summary> + /// <param name="item">Media item.</param> + /// <returns><see cref="IReadOnlyList{BaseItem}" />.</returns> + public static IReadOnlyList<BaseItem> GetTrailers(this IHasTrailers item) + { + var localIds = item.LocalTrailerIds; + var remoteIds = item.RemoteTrailerIds; + var libraryManager = BaseItem.LibraryManager; + + var all = new BaseItem[localIds.Count + remoteIds.Count]; + var index = 0; + foreach (var id in localIds) + { + all[index++] = libraryManager.GetItemById(id); + } + + foreach (var id in remoteIds) + { + all[index++] = libraryManager.GetItemById(id); + } + + return all; + } } } diff --git a/MediaBrowser.Controller/Entities/IItemByName.cs b/MediaBrowser.Controller/Entities/IItemByName.cs index 89b5dfee3..cac8aa61a 100644 --- a/MediaBrowser.Controller/Entities/IItemByName.cs +++ b/MediaBrowser.Controller/Entities/IItemByName.cs @@ -1,9 +1,11 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; namespace MediaBrowser.Controller.Entities { /// <summary> - /// Marker interface + /// Marker interface. /// </summary> public interface IItemByName { diff --git a/MediaBrowser.Controller/Entities/IMetadataContainer.cs b/MediaBrowser.Controller/Entities/IMetadataContainer.cs index a384c0df3..77f5cfb79 100644 --- a/MediaBrowser.Controller/Entities/IMetadataContainer.cs +++ b/MediaBrowser.Controller/Entities/IMetadataContainer.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.Controller/Entities/ISupportsPlaceHolders.cs b/MediaBrowser.Controller/Entities/ISupportsPlaceHolders.cs index 3e96771c3..cdda8ea39 100644 --- a/MediaBrowser.Controller/Entities/ISupportsPlaceHolders.cs +++ b/MediaBrowser.Controller/Entities/ISupportsPlaceHolders.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Entities { public interface ISupportsPlaceHolders diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 78f859069..0baa7725e 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -1,254 +1,365 @@ +#pragma warning disable CA1044, CA1819, CA2227, CS1591 + using System; using System.Collections.Generic; using System.Linq; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Entities { public class InternalItemsQuery { + public InternalItemsQuery() + { + AlbumArtistIds = Array.Empty<Guid>(); + AlbumIds = Array.Empty<Guid>(); + AncestorIds = Array.Empty<Guid>(); + ArtistIds = Array.Empty<Guid>(); + BlockUnratedItems = Array.Empty<UnratedItem>(); + BoxSetLibraryFolders = Array.Empty<Guid>(); + ChannelIds = Array.Empty<Guid>(); + ContributingArtistIds = Array.Empty<Guid>(); + DtoOptions = new DtoOptions(); + EnableTotalRecordCount = true; + ExcludeArtistIds = Array.Empty<Guid>(); + ExcludeInheritedTags = Array.Empty<string>(); + ExcludeItemIds = Array.Empty<Guid>(); + ExcludeItemTypes = Array.Empty<string>(); + ExcludeTags = Array.Empty<string>(); + GenreIds = Array.Empty<Guid>(); + Genres = Array.Empty<string>(); + GroupByPresentationUniqueKey = true; + ImageTypes = Array.Empty<ImageType>(); + IncludeItemTypes = Array.Empty<string>(); + ItemIds = Array.Empty<Guid>(); + MediaTypes = Array.Empty<string>(); + MinSimilarityScore = 20; + OfficialRatings = Array.Empty<string>(); + OrderBy = Array.Empty<ValueTuple<string, SortOrder>>(); + PersonIds = Array.Empty<Guid>(); + PersonTypes = Array.Empty<string>(); + PresetViews = Array.Empty<string>(); + SeriesStatuses = Array.Empty<SeriesStatus>(); + SourceTypes = Array.Empty<SourceType>(); + StudioIds = Array.Empty<Guid>(); + Tags = Array.Empty<string>(); + TopParentIds = Array.Empty<Guid>(); + TrailerTypes = Array.Empty<TrailerType>(); + VideoTypes = Array.Empty<VideoType>(); + Years = Array.Empty<int>(); + } + + public InternalItemsQuery(User? user) + : this() + { + if (user != null) + { + SetUser(user); + } + } + public bool Recursive { get; set; } public int? StartIndex { get; set; } public int? Limit { get; set; } - public User User { get; set; } + public User? User { get; set; } - public BaseItem SimilarTo { get; set; } + public BaseItem? SimilarTo { get; set; } public bool? IsFolder { get; set; } + public bool? IsFavorite { get; set; } + public bool? IsFavoriteOrLiked { get; set; } + public bool? IsLiked { get; set; } + public bool? IsPlayed { get; set; } + public bool? IsResumable { get; set; } + public bool? IncludeItemsByName { get; set; } public string[] MediaTypes { get; set; } + public string[] IncludeItemTypes { get; set; } + public string[] ExcludeItemTypes { get; set; } + public string[] ExcludeTags { get; set; } + public string[] ExcludeInheritedTags { get; set; } - public string[] Genres { get; set; } + + public IReadOnlyList<string> Genres { get; set; } public bool? IsSpecialSeason { get; set; } + public bool? IsMissing { get; set; } + public bool? IsUnaired { get; set; } + public bool? CollapseBoxSetItems { get; set; } - public string NameStartsWithOrGreater { get; set; } - public string NameStartsWith { get; set; } - public string NameLessThan { get; set; } - public string NameContains { get; set; } - public string MinSortName { get; set; } + public string? NameStartsWithOrGreater { get; set; } + + public string? NameStartsWith { get; set; } + + public string? NameLessThan { get; set; } + + public string? NameContains { get; set; } + + public string? MinSortName { get; set; } + + public string? PresentationUniqueKey { get; set; } - public string PresentationUniqueKey { get; set; } - public string Path { get; set; } - public string Name { get; set; } + public string? Path { get; set; } + + public string? Name { get; set; } + + public string? Person { get; set; } - public string Person { get; set; } public Guid[] PersonIds { get; set; } + public Guid[] ItemIds { get; set; } + public Guid[] ExcludeItemIds { get; set; } - public string AdjacentTo { get; set; } + + public string? AdjacentTo { get; set; } + public string[] PersonTypes { get; set; } public bool? Is3D { get; set; } + public bool? IsHD { get; set; } + public bool? IsLocked { get; set; } + public bool? IsPlaceHolder { get; set; } public bool? HasImdbId { get; set; } + public bool? HasOverview { get; set; } + public bool? HasTmdbId { get; set; } + public bool? HasOfficialRating { get; set; } + public bool? HasTvdbId { get; set; } + public bool? HasThemeSong { get; set; } + public bool? HasThemeVideo { get; set; } + public bool? HasSubtitles { get; set; } + public bool? HasSpecialFeature { get; set; } + public bool? HasTrailer { get; set; } + public bool? HasParentalRating { get; set; } public Guid[] StudioIds { get; set; } - public Guid[] GenreIds { get; set; } + + public IReadOnlyList<Guid> GenreIds { get; set; } + public ImageType[] ImageTypes { get; set; } + public VideoType[] VideoTypes { get; set; } + public UnratedItem[] BlockUnratedItems { get; set; } + public int[] Years { get; set; } + public string[] Tags { get; set; } + public string[] OfficialRatings { get; set; } public DateTime? MinPremiereDate { get; set; } + public DateTime? MaxPremiereDate { get; set; } + public DateTime? MinStartDate { get; set; } + public DateTime? MaxStartDate { get; set; } + public DateTime? MinEndDate { get; set; } + public DateTime? MaxEndDate { get; set; } + public bool? IsAiring { get; set; } public bool? IsMovie { get; set; } + public bool? IsSports { get; set; } + public bool? IsKids { get; set; } + public bool? IsNews { get; set; } + public bool? IsSeries { get; set; } + public int? MinIndexNumber { get; set; } + public int? AiredDuringSeason { get; set; } + public double? MinCriticRating { get; set; } + public double? MinCommunityRating { get; set; } - public Guid[] ChannelIds { get; set; } + public IReadOnlyList<Guid> ChannelIds { get; set; } public int? ParentIndexNumber { get; set; } + public int? ParentIndexNumberNotEquals { get; set; } + public int? IndexNumber { get; set; } + public int? MinParentalRating { get; set; } + public int? MaxParentalRating { get; set; } public bool? HasDeadParentId { get; set; } + public bool? IsVirtualItem { get; set; } public Guid ParentId { get; set; } - public string ParentType { get; set; } + + public string? ParentType { get; set; } + public Guid[] AncestorIds { get; set; } - public Guid[] TopParentIds { get; set; } - public BaseItem Parent - { - set - { - if (value == null) - { - ParentId = Guid.Empty; - ParentType = null; - } - else - { - ParentId = value.Id; - ParentType = value.GetType().Name; - } - } - } + public Guid[] TopParentIds { get; set; } public string[] PresetViews { get; set; } + public TrailerType[] TrailerTypes { get; set; } + public SourceType[] SourceTypes { get; set; } public SeriesStatus[] SeriesStatuses { get; set; } - public string ExternalSeriesId { get; set; } - public string ExternalId { get; set; } + + public string? ExternalSeriesId { get; set; } + + public string? ExternalId { get; set; } public Guid[] AlbumIds { get; set; } + public Guid[] ArtistIds { get; set; } + public Guid[] ExcludeArtistIds { get; set; } - public string AncestorWithPresentationUniqueKey { get; set; } - public string SeriesPresentationUniqueKey { get; set; } + + public string? AncestorWithPresentationUniqueKey { get; set; } + + public string? SeriesPresentationUniqueKey { get; set; } public bool GroupByPresentationUniqueKey { get; set; } + public bool GroupBySeriesPresentationUniqueKey { get; set; } + public bool EnableTotalRecordCount { get; set; } + public bool ForceDirect { get; set; } - public Dictionary<string, string> ExcludeProviderIds { get; set; } + + public Dictionary<string, string>? ExcludeProviderIds { get; set; } + public bool EnableGroupByMetadataKey { get; set; } + public bool? HasChapterImages { get; set; } - public ValueTuple<string, SortOrder>[] OrderBy { get; set; } + public IReadOnlyList<(string, SortOrder)> OrderBy { get; set; } public DateTime? MinDateCreated { get; set; } + public DateTime? MinDateLastSaved { get; set; } + public DateTime? MinDateLastSavedForUser { get; set; } public DtoOptions DtoOptions { get; set; } + public int MinSimilarityScore { get; set; } - public string HasNoAudioTrackWithLanguage { get; set; } - public string HasNoInternalSubtitleTrackWithLanguage { get; set; } - public string HasNoExternalSubtitleTrackWithLanguage { get; set; } - public string HasNoSubtitleTrackWithLanguage { get; set; } + + public string? HasNoAudioTrackWithLanguage { get; set; } + + public string? HasNoInternalSubtitleTrackWithLanguage { get; set; } + + public string? HasNoExternalSubtitleTrackWithLanguage { get; set; } + + public string? HasNoSubtitleTrackWithLanguage { get; set; } + public bool? IsDeadArtist { get; set; } + public bool? IsDeadStudio { get; set; } - public bool? IsDeadPerson { get; set; } - public InternalItemsQuery() - { - AlbumArtistIds = Array.Empty<Guid>(); - AlbumIds = Array.Empty<Guid>(); - AncestorIds = Array.Empty<Guid>(); - ArtistIds = Array.Empty<Guid>(); - BlockUnratedItems = Array.Empty<UnratedItem>(); - BoxSetLibraryFolders = Array.Empty<Guid>(); - ChannelIds = Array.Empty<Guid>(); - ContributingArtistIds = Array.Empty<Guid>(); - DtoOptions = new DtoOptions(); - EnableTotalRecordCount = true; - ExcludeArtistIds = Array.Empty<Guid>(); - ExcludeInheritedTags = Array.Empty<string>(); - ExcludeItemIds = Array.Empty<Guid>(); - ExcludeItemTypes = Array.Empty<string>(); - ExcludeProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - ExcludeTags = Array.Empty<string>(); - GenreIds = Array.Empty<Guid>(); - Genres = Array.Empty<string>(); - GroupByPresentationUniqueKey = true; - HasAnyProviderId = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - ImageTypes = Array.Empty<ImageType>(); - IncludeItemTypes = Array.Empty<string>(); - ItemIds = Array.Empty<Guid>(); - MediaTypes = Array.Empty<string>(); - MinSimilarityScore = 20; - OfficialRatings = Array.Empty<string>(); - OrderBy = Array.Empty<ValueTuple<string, SortOrder>>(); - PersonIds = Array.Empty<Guid>(); - PersonTypes = Array.Empty<string>(); - PresetViews = Array.Empty<string>(); - SeriesStatuses = Array.Empty<SeriesStatus>(); - SourceTypes = Array.Empty<SourceType>(); - StudioIds = Array.Empty<Guid>(); - Tags = Array.Empty<string>(); - TopParentIds = Array.Empty<Guid>(); - TrailerTypes = Array.Empty<TrailerType>(); - VideoTypes = Array.Empty<VideoType>(); - Years = Array.Empty<int>(); - } + public bool? IsDeadPerson { get; set; } - public InternalItemsQuery(User user) - : this() - { - SetUser(user); - } + /// <summary> + /// Gets or sets a value indicating whether album sub-folders should be returned if they exist. + /// </summary> + public bool? DisplayAlbumFolders { get; set; } - public void SetUser(User user) + public BaseItem? Parent { - if (user != null) + set { - var policy = user.Policy; - MaxParentalRating = policy.MaxParentalRating; - - if (policy.MaxParentalRating.HasValue) + if (value == null) + { + ParentId = Guid.Empty; + ParentType = null; + } + else { - BlockUnratedItems = policy.BlockUnratedItems.Where(i => i != UnratedItem.Other).ToArray(); + ParentId = value.Id; + ParentType = value.GetType().Name; } - - ExcludeInheritedTags = policy.BlockedTags; - - User = user; } } - public Dictionary<string, string> HasAnyProviderId { get; set; } + public Dictionary<string, string>? HasAnyProviderId { get; set; } + public Guid[] AlbumArtistIds { get; set; } + public Guid[] BoxSetLibraryFolders { get; set; } + public Guid[] ContributingArtistIds { get; set; } + public bool? HasAired { get; set; } + public bool? HasOwnerId { get; set; } + public bool? Is4K { get; set; } + public int? MaxHeight { get; set; } + public int? MaxWidth { get; set; } + public int? MinHeight { get; set; } + public int? MinWidth { get; set; } - public string SearchTerm { get; set; } - public string SeriesTimerId { get; set; } + + public string? SearchTerm { get; set; } + + public string? SeriesTimerId { get; set; } + + public void SetUser(User user) + { + MaxParentalRating = user.MaxParentalAgeRating; + + if (MaxParentalRating.HasValue) + { + string 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); + + User = user; + } } } diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs index 1613531b5..3e1d89274 100644 --- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs @@ -1,20 +1,45 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; +using System.Collections.Generic; +using Jellyfin.Data.Entities; namespace MediaBrowser.Controller.Entities { public class InternalPeopleQuery { + public InternalPeopleQuery() + : this(Array.Empty<string>(), Array.Empty<string>()) + { + } + + public InternalPeopleQuery(IReadOnlyList<string> personTypes, IReadOnlyList<string> excludePersonTypes) + { + PersonTypes = personTypes; + ExcludePersonTypes = excludePersonTypes; + } + + /// <summary> + /// Gets or sets the maximum number of items the query should return. + /// </summary> + public int Limit { get; set; } + public Guid ItemId { get; set; } - public string[] PersonTypes { get; set; } - public string[] ExcludePersonTypes { get; set; } + + public IReadOnlyList<string> PersonTypes { get; } + + public IReadOnlyList<string> ExcludePersonTypes { get; } + public int? MaxListOrder { get; set; } + public Guid AppearsInItemId { get; set; } + public string NameContains { get; set; } - public InternalPeopleQuery() - { - PersonTypes = Array.Empty<string>(); - ExcludePersonTypes = Array.Empty<string>(); - } + public User User { get; set; } + + public bool? IsFavorite { get; set; } } } diff --git a/MediaBrowser.Controller/Entities/ItemImageInfo.cs b/MediaBrowser.Controller/Entities/ItemImageInfo.cs index ff6b13398..ea8555dbf 100644 --- a/MediaBrowser.Controller/Entities/ItemImageInfo.cs +++ b/MediaBrowser.Controller/Entities/ItemImageInfo.cs @@ -1,6 +1,10 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; +using System.Text.Json.Serialization; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { @@ -25,22 +29,16 @@ namespace MediaBrowser.Controller.Entities public DateTime DateModified { get; set; } public int Width { get; set; } + public int Height { get; set; } - [IgnoreDataMember] - public bool IsLocalFile - { - get - { - if (Path != null) - { - if (Path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - } - return true; - } - } + /// <summary> + /// Gets or sets the blurhash. + /// </summary> + /// <value>The blurhash.</value> + public string BlurHash { get; set; } + + [JsonIgnore] + public bool IsLocalFile => Path == null || !Path.StartsWith("http", StringComparison.OrdinalIgnoreCase); } } diff --git a/MediaBrowser.Controller/Entities/LinkedChild.cs b/MediaBrowser.Controller/Entities/LinkedChild.cs index bb2d03246..fd5fef3dc 100644 --- a/MediaBrowser.Controller/Entities/LinkedChild.cs +++ b/MediaBrowser.Controller/Entities/LinkedChild.cs @@ -1,21 +1,31 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; -using System.Collections.Generic; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; +using System.Globalization; +using System.Text.Json.Serialization; namespace MediaBrowser.Controller.Entities { public class LinkedChild { + public LinkedChild() + { + Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); + } + public string Path { get; set; } + public LinkedChildType Type { get; set; } + public string LibraryItemId { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string Id { get; set; } /// <summary> - /// Serves as a cache + /// Gets or sets the linked item id. /// </summary> public Guid? ItemId { get; set; } @@ -29,45 +39,10 @@ namespace MediaBrowser.Controller.Entities if (string.IsNullOrEmpty(child.Path)) { - child.LibraryItemId = item.Id.ToString("N"); + child.LibraryItemId = item.Id.ToString("N", CultureInfo.InvariantCulture); } return child; } - - public LinkedChild() - { - Id = Guid.NewGuid().ToString("N"); - } - } - - public enum LinkedChildType - { - Manual = 0, - Shortcut = 1 - } - - public class LinkedChildComparer : IEqualityComparer<LinkedChild> - { - private readonly IFileSystem _fileSystem; - - public LinkedChildComparer(IFileSystem fileSystem) - { - _fileSystem = fileSystem; - } - - public bool Equals(LinkedChild x, LinkedChild y) - { - if (x.Type == y.Type) - { - return _fileSystem.AreEqual(x.Path, y.Path); - } - return false; - } - - public int GetHashCode(LinkedChild obj) - { - return ((obj.Path ?? string.Empty) + (obj.LibraryItemId ?? string.Empty) + obj.Type).GetHashCode(); - } } } diff --git a/MediaBrowser.Controller/Entities/LinkedChildComparer.cs b/MediaBrowser.Controller/Entities/LinkedChildComparer.cs new file mode 100644 index 000000000..4e58e2942 --- /dev/null +++ b/MediaBrowser.Controller/Entities/LinkedChildComparer.cs @@ -0,0 +1,35 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using MediaBrowser.Model.IO; + +namespace MediaBrowser.Controller.Entities +{ + public class LinkedChildComparer : IEqualityComparer<LinkedChild> + { + private readonly IFileSystem _fileSystem; + + public LinkedChildComparer(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } + + public bool Equals(LinkedChild x, LinkedChild y) + { + if (x.Type == y.Type) + { + return _fileSystem.AreEqual(x.Path, y.Path); + } + + return false; + } + + public int GetHashCode(LinkedChild obj) + { + return ((obj.Path ?? string.Empty) + (obj.LibraryItemId ?? string.Empty) + obj.Type).GetHashCode(StringComparison.Ordinal); + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Entities/LinkedChildType.cs b/MediaBrowser.Controller/Entities/LinkedChildType.cs new file mode 100644 index 000000000..9ddb7b620 --- /dev/null +++ b/MediaBrowser.Controller/Entities/LinkedChildType.cs @@ -0,0 +1,18 @@ +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// The linked child type. + /// </summary> + public enum LinkedChildType + { + /// <summary> + /// Manually linked child. + /// </summary> + Manual = 0, + + /// <summary> + /// Shortcut linked child. + /// </summary> + Shortcut = 1 + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index 124a943ef..e46f99cd5 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -1,40 +1,47 @@ +#nullable disable + +#pragma warning disable CA1721, CA1819, CS1591 + using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; namespace MediaBrowser.Controller.Entities.Movies { /// <summary> - /// Class BoxSet + /// Class BoxSet. /// </summary> public class BoxSet : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<BoxSetInfo> { public BoxSet() { - RemoteTrailers = EmptyMediaUrlArray; + RemoteTrailers = Array.Empty<MediaUrl>(); LocalTrailerIds = Array.Empty<Guid>(); RemoteTrailerIds = Array.Empty<Guid>(); DisplayOrder = ItemSortBy.PremiereDate; } - [IgnoreDataMember] + [JsonIgnore] protected override bool FilterLinkedChildrenPerUser => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => true; - public Guid[] LocalTrailerIds { get; set; } - public Guid[] RemoteTrailerIds { get; set; } + /// <inheritdoc /> + public IReadOnlyList<Guid> LocalTrailerIds { get; set; } + + /// <inheritdoc /> + public IReadOnlyList<Guid> RemoteTrailerIds { get; set; } /// <summary> /// Gets or sets the display order. @@ -42,9 +49,33 @@ namespace MediaBrowser.Controller.Entities.Movies /// <value>The display order.</value> public string DisplayOrder { get; set; } - protected override bool GetBlockUnratedValue(UserPolicy config) + [JsonIgnore] + private bool IsLegacyBoxSet + { + get + { + if (string.IsNullOrEmpty(Path)) + { + return false; + } + + if (LinkedChildren.Length > 0) + { + return false; + } + + return !FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, Path); + } + } + + [JsonIgnore] + public override bool IsPreSorted => true; + + public Guid[] LibraryFolderIds { get; set; } + + protected override bool GetBlockUnratedValue(User user) { - return config.BlockUnratedItems.Contains(UnratedItem.Movie); + return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Movie); } public override double GetDefaultPrimaryImageAspectRatio() @@ -61,7 +92,8 @@ namespace MediaBrowser.Controller.Entities.Movies { return base.GetNonCachedChildren(directoryService); } - return new List<BaseItem>(); + + return Enumerable.Empty<BaseItem>(); } protected override List<BaseItem> LoadChildren() @@ -75,28 +107,6 @@ namespace MediaBrowser.Controller.Entities.Movies return new List<BaseItem>(); } - [IgnoreDataMember] - private bool IsLegacyBoxSet - { - get - { - if (string.IsNullOrEmpty(Path)) - { - return false; - } - - if (LinkedChildren.Length > 0) - { - return false; - } - - return !FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, Path); - } - } - - [IgnoreDataMember] - public override bool IsPreSorted => true; - public override bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders) { return true; @@ -183,8 +193,6 @@ namespace MediaBrowser.Controller.Entities.Movies return IsVisible(user); } - public Guid[] LibraryFolderIds { get; set; } - private Guid[] GetLibraryFolderIds(User user) { return LibraryManager.GetUserRootFolder().GetChildren(user, true) @@ -194,7 +202,7 @@ namespace MediaBrowser.Controller.Entities.Movies public Guid[] GetLibraryFolderIds() { - var expandedFolders = new List<Guid>() { }; + var expandedFolders = new List<Guid>(); return FlattenItems(this, expandedFolders) .SelectMany(i => LibraryManager.GetCollectionFolders(i)) @@ -211,8 +219,7 @@ namespace MediaBrowser.Controller.Entities.Movies private IEnumerable<BaseItem> FlattenItems(BaseItem item, List<Guid> expandedFolders) { - var boxset = item as BoxSet; - if (boxset != null) + if (item is BoxSet boxset) { if (!expandedFolders.Contains(item.Id)) { diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index 232d11624..b54bbf5eb 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -1,34 +1,43 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities.Movies { /// <summary> - /// Class Movie + /// Class Movie. /// </summary> public class Movie : Video, IHasSpecialFeatures, IHasTrailers, IHasLookupInfo<MovieInfo>, ISupportsBoxSetGrouping { - public Guid[] SpecialFeatureIds { get; set; } - public Movie() { - SpecialFeatureIds = new Guid[] { }; - RemoteTrailers = EmptyMediaUrlArray; - LocalTrailerIds = new Guid[] { }; - RemoteTrailerIds = new Guid[] { }; + SpecialFeatureIds = Array.Empty<Guid>(); + RemoteTrailers = Array.Empty<MediaUrl>(); + LocalTrailerIds = Array.Empty<Guid>(); + RemoteTrailerIds = Array.Empty<Guid>(); } - public Guid[] LocalTrailerIds { get; set; } - public Guid[] RemoteTrailerIds { get; set; } + /// <inheritdoc /> + public IReadOnlyList<Guid> SpecialFeatureIds { get; set; } + + /// <inheritdoc /> + public IReadOnlyList<Guid> LocalTrailerIds { get; set; } + + /// <inheritdoc /> + public IReadOnlyList<Guid> RemoteTrailerIds { get; set; } /// <summary> /// Gets or sets the name of the TMDB collection. @@ -36,13 +45,16 @@ namespace MediaBrowser.Controller.Entities.Movies /// <value>The name of the TMDB collection.</value> public string TmdbCollectionName { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string CollectionName { get => TmdbCollectionName; set => TmdbCollectionName = value; } + [JsonIgnore] + public override bool StopRefreshIfLocalMetadataFound => false; + public override double GetDefaultPrimaryImageAspectRatio() { // hack for tv plugins @@ -102,6 +114,7 @@ namespace MediaBrowser.Controller.Entities.Movies return itemsChanged; } + /// <inheritdoc /> public override UnratedItem GetBlockUnratedType() { return UnratedItem.Movie; @@ -130,9 +143,10 @@ namespace MediaBrowser.Controller.Entities.Movies return info; } - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + /// <inheritdoc /> + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); if (!ProductionYear.HasValue) { @@ -166,24 +180,22 @@ namespace MediaBrowser.Controller.Entities.Movies return hasChanges; } + /// <inheritdoc /> public override List<ExternalUrl> GetRelatedUrls() { var list = base.GetRelatedUrls(); - var imdbId = this.GetProviderId(MetadataProviders.Imdb); + var imdbId = this.GetProviderId(MetadataProvider.Imdb); if (!string.IsNullOrEmpty(imdbId)) { list.Add(new ExternalUrl { Name = "Trakt", - Url = string.Format("https://trakt.tv/movies/{0}", imdbId) + Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/movies/{0}", imdbId) }); } return list; } - - [IgnoreDataMember] - public override bool StopRefreshIfLocalMetadataFound => false; } } diff --git a/MediaBrowser.Controller/Entities/MusicVideo.cs b/MediaBrowser.Controller/Entities/MusicVideo.cs index 5bf082b7e..237ad5198 100644 --- a/MediaBrowser.Controller/Entities/MusicVideo.cs +++ b/MediaBrowser.Controller/Entities/MusicVideo.cs @@ -1,23 +1,26 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { public class MusicVideo : Video, IHasArtist, IHasMusicGenres, IHasLookupInfo<MusicVideoInfo> { - [IgnoreDataMember] - public string[] Artists { get; set; } - public MusicVideo() { Artists = Array.Empty<string>(); } - [IgnoreDataMember] - public string[] AllArtists => Artists; + /// <inheritdoc /> + [JsonIgnore] + public IReadOnlyList<string> Artists { get; set; } public override UnratedItem GetBlockUnratedType() { @@ -33,9 +36,9 @@ namespace MediaBrowser.Controller.Entities return info; } - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); if (!ProductionYear.HasValue) { diff --git a/MediaBrowser.Controller/Entities/PeopleHelper.cs b/MediaBrowser.Controller/Entities/PeopleHelper.cs index 2fb613768..687ce1ec8 100644 --- a/MediaBrowser.Controller/Entities/PeopleHelper.cs +++ b/MediaBrowser.Controller/Entities/PeopleHelper.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; @@ -98,22 +100,5 @@ namespace MediaBrowser.Controller.Entities existing.SetProviderId(id.Key, id.Value); } } - - public static bool ContainsPerson(List<PersonInfo> people, string name) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - foreach (var i in people) - { - if (string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - return false; - } } } diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs index dd0183489..045c1b89f 100644 --- a/MediaBrowser.Controller/Entities/Person.cs +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -1,9 +1,12 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; -using MediaBrowser.Controller.Extensions; +using System.Text.Json.Serialization; +using Diacritics.Extensions; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities @@ -13,6 +16,26 @@ namespace MediaBrowser.Controller.Entities /// </summary> public class Person : BaseItem, IItemByName, IHasLookupInfo<PersonLookupInfo> { + /// <summary> + /// Gets the folder containing the item. + /// If the item is a folder, it returns the folder itself. + /// </summary> + /// <value>The containing folder path.</value> + [JsonIgnore] + public override string ContainingFolderPath => Path; + + /// <summary> + /// Gets a value indicating whether to enable alpha numeric sorting. + /// </summary> + [JsonIgnore] + public override bool EnableAlphaNumericSorting => false; + + [JsonIgnore] + public override bool SupportsPeople => false; + + [JsonIgnore] + public override bool SupportsAncestors => false; + public override List<string> GetUserDataKeys() { var list = base.GetUserDataKeys(); @@ -20,6 +43,7 @@ namespace MediaBrowser.Controller.Entities list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics()); return list; } + public override string CreatePresentationUniqueKey() { return GetUserDataKeys()[0]; @@ -45,14 +69,6 @@ namespace MediaBrowser.Controller.Entities return LibraryManager.GetItemList(query); } - /// <summary> - /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself - /// </summary> - /// <value>The containing folder path.</value> - [IgnoreDataMember] - public override string ContainingFolderPath => Path; - public override bool CanDelete() { return false; @@ -63,15 +79,6 @@ namespace MediaBrowser.Controller.Entities return true; } - [IgnoreDataMember] - public override bool EnableAlphaNumericSorting => false; - - [IgnoreDataMember] - public override bool SupportsPeople => false; - - [IgnoreDataMember] - public override bool SupportsAncestors => false; - public static string GetPath(string name) { return GetPath(name, true); @@ -115,15 +122,18 @@ namespace MediaBrowser.Controller.Entities Logger.LogDebug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); return true; } + return base.RequiresRefresh(); } /// <summary> - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true or false indicating if changes were made. /// </summary> - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + /// <param name="replaceAllMetadata"><c>true</c> to replace all metadata, <c>false</c> to not.</param> + /// <returns><c>true</c> if changes were made, <c>false</c> if not.</returns> + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); var newPath = GetRebasedPath(); if (!string.Equals(Path, newPath, StringComparison.Ordinal)) @@ -135,57 +145,4 @@ namespace MediaBrowser.Controller.Entities return hasChanges; } } - - /// <summary> - /// This is the small Person stub that is attached to BaseItems - /// </summary> - public class PersonInfo : IHasProviderIds - { - public PersonInfo() - { - ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } - - public Guid ItemId { get; set; } - - /// <summary> - /// Gets or sets the name. - /// </summary> - /// <value>The name.</value> - public string Name { get; set; } - /// <summary> - /// Gets or sets the role. - /// </summary> - /// <value>The role.</value> - public string Role { get; set; } - /// <summary> - /// Gets or sets the type. - /// </summary> - /// <value>The type.</value> - public string Type { get; set; } - - /// <summary> - /// Gets or sets the sort order - ascending - /// </summary> - /// <value>The sort order.</value> - public int? SortOrder { get; set; } - - public string ImageUrl { get; set; } - - public Dictionary<string, string> ProviderIds { get; set; } - - /// <summary> - /// Returns a <see cref="string" /> that represents this instance. - /// </summary> - /// <returns>A <see cref="string" /> that represents this instance.</returns> - public override string ToString() - { - return Name; - } - - public bool IsType(string type) - { - return string.Equals(Type, type, StringComparison.OrdinalIgnoreCase) || string.Equals(Role, type, StringComparison.OrdinalIgnoreCase); - } - } } diff --git a/MediaBrowser.Controller/Entities/PersonInfo.cs b/MediaBrowser.Controller/Entities/PersonInfo.cs new file mode 100644 index 000000000..2b689ae7e --- /dev/null +++ b/MediaBrowser.Controller/Entities/PersonInfo.cs @@ -0,0 +1,66 @@ +#nullable disable + +#pragma warning disable CA2227, CS1591 + +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// This is a small Person stub that is attached to BaseItems. + /// </summary> + public sealed class PersonInfo : IHasProviderIds + { + public PersonInfo() + { + ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + + public Guid ItemId { get; set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string Name { get; set; } + + /// <summary> + /// Gets or sets the role. + /// </summary> + /// <value>The role.</value> + public string Role { get; set; } + + /// <summary> + /// Gets or sets the type. + /// </summary> + /// <value>The type.</value> + public string Type { get; set; } + + /// <summary> + /// Gets or sets the ascending sort order. + /// </summary> + /// <value>The sort order.</value> + public int? SortOrder { get; set; } + + public string ImageUrl { get; set; } + + public Dictionary<string, string> ProviderIds { get; set; } + + /// <summary> + /// Returns a <see cref="string" /> that represents this instance. + /// </summary> + /// <returns>A <see cref="string" /> that represents this instance.</returns> + public override string ToString() + { + return Name; + } + + public bool IsType(string type) + { + return string.Equals(Type, type, StringComparison.OrdinalIgnoreCase) + || string.Equals(Role, type, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/MediaBrowser.Controller/Entities/Photo.cs b/MediaBrowser.Controller/Entities/Photo.cs index 60c832189..ba6ce189a 100644 --- a/MediaBrowser.Controller/Entities/Photo.cs +++ b/MediaBrowser.Controller/Entities/Photo.cs @@ -1,21 +1,24 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System.Text.Json.Serialization; using MediaBrowser.Model.Drawing; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { public class Photo : BaseItem { - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsLocalMetadata => false; - [IgnoreDataMember] + [JsonIgnore] public override string MediaType => Model.Entities.MediaType.Photo; - [IgnoreDataMember] + [JsonIgnore] public override Folder LatestItemsIndexContainer => AlbumEntity; - - [IgnoreDataMember] + [JsonIgnore] public PhotoAlbum AlbumEntity { get @@ -23,16 +26,40 @@ namespace MediaBrowser.Controller.Entities var parents = GetParents(); foreach (var parent in parents) { - var photoAlbum = parent as PhotoAlbum; - if (photoAlbum != null) + if (parent is PhotoAlbum photoAlbum) { return photoAlbum; } } + return null; } } + public string CameraMake { get; set; } + + public string CameraModel { get; set; } + + public string Software { get; set; } + + public double? ExposureTime { get; set; } + + public double? FocalLength { get; set; } + + public ImageOrientation? Orientation { get; set; } + + public double? Aperture { get; set; } + + public double? ShutterSpeed { get; set; } + + public double? Latitude { get; set; } + + public double? Longitude { get; set; } + + public double? Altitude { get; set; } + + public int? IsoSpeedRating { get; set; } + public override bool CanDownload() { return true; @@ -41,10 +68,10 @@ namespace MediaBrowser.Controller.Entities public override double GetDefaultPrimaryImageAspectRatio() { // REVIEW: @bond - if (Width.HasValue && Height.HasValue) + if (Width != 0 && Height != 0) { - double width = Width.Value; - double height = Height.Value; + double width = Width; + double height = Height; if (Orientation.HasValue) { @@ -66,21 +93,5 @@ namespace MediaBrowser.Controller.Entities return base.GetDefaultPrimaryImageAspectRatio(); } - - public new int? Width { get; set; } - public new int? Height { get; set; } - public string CameraMake { get; set; } - public string CameraModel { get; set; } - public string Software { get; set; } - public double? ExposureTime { get; set; } - public double? FocalLength { get; set; } - public ImageOrientation? Orientation { get; set; } - public double? Aperture { get; set; } - public double? ShutterSpeed { get; set; } - - public double? Latitude { get; set; } - public double? Longitude { get; set; } - public double? Altitude { get; set; } - public int? IsoSpeedRating { get; set; } } } diff --git a/MediaBrowser.Controller/Entities/PhotoAlbum.cs b/MediaBrowser.Controller/Entities/PhotoAlbum.cs index 4cd0c8b66..a7ecb9061 100644 --- a/MediaBrowser.Controller/Entities/PhotoAlbum.cs +++ b/MediaBrowser.Controller/Entities/PhotoAlbum.cs @@ -1,16 +1,18 @@ -using MediaBrowser.Model.Serialization; +#pragma warning disable CS1591 + +using System.Text.Json.Serialization; namespace MediaBrowser.Controller.Entities { public class PhotoAlbum : Folder { - [IgnoreDataMember] + [JsonIgnore] public override bool AlwaysScanInternalMetadataPath => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; } } diff --git a/MediaBrowser.Controller/Entities/Share.cs b/MediaBrowser.Controller/Entities/Share.cs index c17789ccc..64f446eef 100644 --- a/MediaBrowser.Controller/Entities/Share.cs +++ b/MediaBrowser.Controller/Entities/Share.cs @@ -1,13 +1,13 @@ +#nullable disable + +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Entities { - public interface IHasShares - { - Share[] Shares { get; set; } - } - public class Share { public string UserId { get; set; } + public bool CanEdit { get; set; } } } diff --git a/MediaBrowser.Controller/Entities/SourceType.cs b/MediaBrowser.Controller/Entities/SourceType.cs index 927483b93..be19e1bda 100644 --- a/MediaBrowser.Controller/Entities/SourceType.cs +++ b/MediaBrowser.Controller/Entities/SourceType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Entities { public enum SourceType diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs index d6da0d48c..c8feb1c94 100644 --- a/MediaBrowser.Controller/Entities/Studio.cs +++ b/MediaBrowser.Controller/Entities/Studio.cs @@ -1,16 +1,37 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; -using MediaBrowser.Controller.Extensions; -using MediaBrowser.Model.Serialization; +using System.Text.Json.Serialization; +using Diacritics.Extensions; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities { /// <summary> - /// Class Studio + /// Class Studio. /// </summary> public class Studio : BaseItem, IItemByName { + /// <summary> + /// Gets the folder containing the item. + /// If the item is a folder, it returns the folder itself. + /// </summary> + /// <value>The containing folder path.</value> + [JsonIgnore] + public override string ContainingFolderPath => Path; + + [JsonIgnore] + public override bool IsDisplayedAsFolder => true; + + [JsonIgnore] + public override bool SupportsAncestors => false; + + [JsonIgnore] + public override bool SupportsPeople => false; + public override List<string> GetUserDataKeys() { var list = base.GetUserDataKeys(); @@ -18,25 +39,12 @@ namespace MediaBrowser.Controller.Entities list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics()); return list; } + public override string CreatePresentationUniqueKey() { return GetUserDataKeys()[0]; } - /// <summary> - /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself - /// </summary> - /// <value>The containing folder path.</value> - [IgnoreDataMember] - public override string ContainingFolderPath => Path; - - [IgnoreDataMember] - public override bool IsDisplayedAsFolder => true; - - [IgnoreDataMember] - public override bool SupportsAncestors => false; - public override double GetDefaultPrimaryImageAspectRatio() { double value = 16; @@ -62,9 +70,6 @@ namespace MediaBrowser.Controller.Entities return LibraryManager.GetItemList(query); } - [IgnoreDataMember] - public override bool SupportsPeople => false; - public static string GetPath(string name) { return GetPath(name, true); @@ -93,15 +98,18 @@ namespace MediaBrowser.Controller.Entities Logger.LogDebug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); return true; } + return base.RequiresRefresh(); } /// <summary> - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true or false indicating if changes were made. /// </summary> - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + /// <param name="replaceAllMetadata"><c>true</c> to replace all metadata, <c>false</c> to not.</param> + /// <returns><c>true</c> if changes were made, <c>false</c> if not.</returns> + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); var newPath = GetRebasedPath(); if (!string.Equals(Path, newPath, StringComparison.Ordinal)) diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 072b1d89a..27c3ff81b 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -1,107 +1,80 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities.TV { /// <summary> - /// Class Episode + /// Class Episode. /// </summary> public class Episode : Video, IHasTrailers, IHasLookupInfo<EpisodeInfo>, IHasSeries { public Episode() { - RemoteTrailers = EmptyMediaUrlArray; + RemoteTrailers = Array.Empty<MediaUrl>(); LocalTrailerIds = Array.Empty<Guid>(); RemoteTrailerIds = Array.Empty<Guid>(); } - public Guid[] LocalTrailerIds { get; set; } - public Guid[] RemoteTrailerIds { get; set; } + /// <inheritdoc /> + public IReadOnlyList<Guid> LocalTrailerIds { get; set; } + + /// <inheritdoc /> + public IReadOnlyList<Guid> RemoteTrailerIds { get; set; } /// <summary> - /// Gets the season in which it aired. + /// Gets or sets the season in which it aired. /// </summary> /// <value>The aired season.</value> public int? AirsBeforeSeasonNumber { get; set; } + public int? AirsAfterSeasonNumber { get; set; } + public int? AirsBeforeEpisodeNumber { get; set; } /// <summary> - /// This is the ending episode number for double episodes. + /// Gets or sets the ending episode number for double episodes. /// </summary> /// <value>The index number.</value> public int? IndexNumberEnd { get; set; } - public string FindSeriesSortName() - { - var series = Series; - return series == null ? SeriesName : series.SortName; - } - - [IgnoreDataMember] + [JsonIgnore] protected override bool SupportsOwnedItems => IsStacked || MediaSourceCount > 1; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => true; - [IgnoreDataMember] + [JsonIgnore] public int? AiredSeasonNumber => AirsAfterSeasonNumber ?? AirsBeforeSeasonNumber ?? ParentIndexNumber; - [IgnoreDataMember] + [JsonIgnore] public override Folder LatestItemsIndexContainer => Series; - [IgnoreDataMember] + [JsonIgnore] public override Guid DisplayParentId => SeasonId; - [IgnoreDataMember] + [JsonIgnore] protected override bool EnableDefaultVideoUserDataKeys => false; - public override double GetDefaultPrimaryImageAspectRatio() - { - // hack for tv plugins - if (SourceType == SourceType.Channel) - { - return 0; - } - - return 16.0 / 9; - } - - public override List<string> GetUserDataKeys() - { - var list = base.GetUserDataKeys(); - - var series = Series; - if (series != null && ParentIndexNumber.HasValue && IndexNumber.HasValue) - { - var seriesUserDataKeys = series.GetUserDataKeys(); - var take = seriesUserDataKeys.Count; - if (seriesUserDataKeys.Count > 1) - { - take--; - } - list.InsertRange(0, seriesUserDataKeys.Take(take).Select(i => i + ParentIndexNumber.Value.ToString("000") + IndexNumber.Value.ToString("000"))); - } - - return list; - } - /// <summary> - /// This Episode's Series Instance + /// Gets the Episode's Series Instance. /// </summary> /// <value>The series.</value> - [IgnoreDataMember] + [JsonIgnore] public Series Series { get @@ -111,11 +84,12 @@ namespace MediaBrowser.Controller.Entities.TV { seriesId = FindSeriesId(); } + return !seriesId.Equals(Guid.Empty) ? (LibraryManager.GetItemById(seriesId) as Series) : null; } } - [IgnoreDataMember] + [JsonIgnore] public Season Season { get @@ -125,22 +99,91 @@ namespace MediaBrowser.Controller.Entities.TV { seasonId = FindSeasonId(); } + return !seasonId.Equals(Guid.Empty) ? (LibraryManager.GetItemById(seasonId) as Season) : null; } } - [IgnoreDataMember] + [JsonIgnore] public bool IsInSeasonFolder => FindParent<Season>() != null; - [IgnoreDataMember] + [JsonIgnore] public string SeriesPresentationUniqueKey { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string SeriesName { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string SeasonName { get; set; } + [JsonIgnore] + public override bool SupportsRemoteImageDownloading + { + get + { + if (IsMissingEpisode) + { + return false; + } + + return true; + } + } + + [JsonIgnore] + public bool IsMissingEpisode => LocationType == LocationType.Virtual; + + [JsonIgnore] + public Guid SeasonId { get; set; } + + [JsonIgnore] + public Guid SeriesId { get; set; } + + public string FindSeriesSortName() + { + var series = Series; + return series == null ? SeriesName : series.SortName; + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + // hack for tv plugins + if (SourceType == SourceType.Channel) + { + return 0; + } + + return 16.0 / 9; + } + + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + var series = Series; + if (series != null && ParentIndexNumber.HasValue && IndexNumber.HasValue) + { + var seriesUserDataKeys = series.GetUserDataKeys(); + var take = seriesUserDataKeys.Count; + if (seriesUserDataKeys.Count > 1) + { + take--; + } + + var newList = seriesUserDataKeys.GetRange(0, take); + var suffix = ParentIndexNumber.Value.ToString("000", CultureInfo.InvariantCulture) + IndexNumber.Value.ToString("000", CultureInfo.InvariantCulture); + for (int i = 0; i < take; i++) + { + newList[i] = newList[i] + suffix; + } + + newList.AddRange(list); + list = newList; + } + + return list; + } + public string FindSeriesPresentationUniqueKey() { var series = Series; @@ -157,6 +200,7 @@ namespace MediaBrowser.Controller.Entities.TV { return "Season " + ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture); } + return "Season Unknown"; } @@ -197,8 +241,8 @@ namespace MediaBrowser.Controller.Entities.TV /// <returns>System.String.</returns> protected override string CreateSortName() { - return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("000 - ") : "") - + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ") : "") + Name; + return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("000 - ", CultureInfo.InvariantCulture) : string.Empty) + + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) + Name; } /// <summary> @@ -221,28 +265,6 @@ namespace MediaBrowser.Controller.Entities.TV return false; } - [IgnoreDataMember] - public override bool SupportsRemoteImageDownloading - { - get - { - if (IsMissingEpisode) - { - return false; - } - - return true; - } - } - - [IgnoreDataMember] - public bool IsMissingEpisode => LocationType == LocationType.Virtual; - - [IgnoreDataMember] - public Guid SeasonId { get; set; } - [IgnoreDataMember] - public Guid SeriesId { get; set; } - public Guid FindSeriesId() { var series = FindParent<Series>(); @@ -265,7 +287,8 @@ namespace MediaBrowser.Controller.Entities.TV public override IEnumerable<FileSystemMetadata> GetDeletePaths() { - return new[] { + return new[] + { new FileSystemMetadata { FullName = Path, @@ -297,9 +320,9 @@ namespace MediaBrowser.Controller.Entities.TV return id; } - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); if (!IsLocked) { @@ -307,7 +330,7 @@ namespace MediaBrowser.Controller.Entities.TV { try { - if (LibraryManager.FillMissingEpisodeNumbersFromPath(this, replaceAllMetdata)) + if (LibraryManager.FillMissingEpisodeNumbersFromPath(this, replaceAllMetadata)) { hasChanges = true; } diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 5d7c260d1..926c7b045 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -1,38 +1,87 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; namespace MediaBrowser.Controller.Entities.TV { /// <summary> - /// Class Season + /// Class Season. /// </summary> public class Season : Folder, IHasSeries, IHasLookupInfo<SeasonInfo> { - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsAddingToPlaylist => true; - [IgnoreDataMember] + [JsonIgnore] public override bool IsPreSorted => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsDateLastMediaAdded => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => true; - [IgnoreDataMember] + [JsonIgnore] public override Guid DisplayParentId => SeriesId; + /// <summary> + /// Gets this Episode's Series Instance. + /// </summary> + /// <value>The series.</value> + [JsonIgnore] + public Series Series + { + get + { + var seriesId = SeriesId; + if (seriesId == Guid.Empty) + { + seriesId = FindSeriesId(); + } + + return seriesId == Guid.Empty ? null : (LibraryManager.GetItemById(seriesId) as Series); + } + } + + [JsonIgnore] + public string SeriesPath + { + get + { + var series = Series; + + if (series != null) + { + return series.Path; + } + + return System.IO.Path.GetDirectoryName(Path); + } + } + + [JsonIgnore] + public string SeriesPresentationUniqueKey { get; set; } + + [JsonIgnore] + public string SeriesName { get; set; } + + [JsonIgnore] + public Guid SeriesId { get; set; } + public override double GetDefaultPrimaryImageAspectRatio() { double value = 2; @@ -54,7 +103,15 @@ namespace MediaBrowser.Controller.Entities.TV var series = Series; if (series != null) { - list.InsertRange(0, series.GetUserDataKeys().Select(i => i + (IndexNumber ?? 0).ToString("000"))); + var newList = series.GetUserDataKeys(); + var suffix = (IndexNumber ?? 0).ToString("000", CultureInfo.InvariantCulture); + for (int i = 0; i < newList.Count; i++) + { + newList[i] = newList[i] + suffix; + } + + newList.AddRange(list); + list = newList; } return list; @@ -67,40 +124,6 @@ namespace MediaBrowser.Controller.Entities.TV return result; } - /// <summary> - /// This Episode's Series Instance - /// </summary> - /// <value>The series.</value> - [IgnoreDataMember] - public Series Series - { - get - { - var seriesId = SeriesId; - if (seriesId == Guid.Empty) - { - seriesId = FindSeriesId(); - } - return seriesId == Guid.Empty ? null : (LibraryManager.GetItemById(seriesId) as Series); - } - } - - [IgnoreDataMember] - public string SeriesPath - { - get - { - var series = Series; - - if (series != null) - { - return series.Path; - } - - return System.IO.Path.GetDirectoryName(Path); - } - } - public override string CreatePresentationUniqueKey() { if (IndexNumber.HasValue) @@ -108,7 +131,7 @@ namespace MediaBrowser.Controller.Entities.TV var series = Series; if (series != null) { - return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000"); + return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000", CultureInfo.InvariantCulture); } } @@ -121,7 +144,7 @@ namespace MediaBrowser.Controller.Entities.TV /// <returns>System.String.</returns> protected override string CreateSortName() { - return IndexNumber != null ? IndexNumber.Value.ToString("0000") : Name; + return IndexNumber != null ? IndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) : Name; } protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) @@ -143,6 +166,9 @@ namespace MediaBrowser.Controller.Entities.TV /// <summary> /// Gets the episodes. /// </summary> + /// <param name="user">The user.</param> + /// <param name="options">The options to use.</param> + /// <returns>Set of episodes.</returns> public List<BaseItem> GetEpisodes(User user, DtoOptions options) { return GetEpisodes(Series, user, options); @@ -168,7 +194,7 @@ namespace MediaBrowser.Controller.Entities.TV return GetEpisodes(user, new DtoOptions(true)); } - protected override bool GetBlockUnratedValue(UserPolicy config) + protected override bool GetBlockUnratedValue(User user) { // Don't block. Let either the entire series rating or episode rating determine it return false; @@ -179,15 +205,6 @@ namespace MediaBrowser.Controller.Entities.TV return UnratedItem.Series; } - [IgnoreDataMember] - public string SeriesPresentationUniqueKey { get; set; } - - [IgnoreDataMember] - public string SeriesName { get; set; } - - [IgnoreDataMember] - public Guid SeriesId { get; set; } - public string FindSeriesPresentationUniqueKey() { var series = Series; @@ -203,7 +220,7 @@ namespace MediaBrowser.Controller.Entities.TV public Guid FindSeriesId() { var series = FindParent<Series>(); - return series == null ? Guid.Empty : series.Id; + return series?.Id ?? Guid.Empty; } /// <summary> @@ -225,16 +242,17 @@ namespace MediaBrowser.Controller.Entities.TV } /// <summary> - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true or false indicating if changes were made. /// </summary> + /// <param name="replaceAllMetadata"><c>true</c> to replace metdata, <c>false</c> to not.</param> /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path)) { - IndexNumber = IndexNumber ?? LibraryManager.GetSeasonNumberFromPath(Path); + IndexNumber ??= LibraryManager.GetSeasonNumberFromPath(Path); // 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 570e9389e..e4933e968 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -1,57 +1,69 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; +using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider; namespace MediaBrowser.Controller.Entities.TV { /// <summary> - /// Class Series + /// Class Series. /// </summary> public class Series : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<SeriesInfo>, IMetadataContainer { public Series() { - RemoteTrailers = EmptyMediaUrlArray; + RemoteTrailers = Array.Empty<MediaUrl>(); LocalTrailerIds = Array.Empty<Guid>(); RemoteTrailerIds = Array.Empty<Guid>(); AirDays = Array.Empty<DayOfWeek>(); } public DayOfWeek[] AirDays { get; set; } + public string AirTime { get; set; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsAddingToPlaylist => true; - [IgnoreDataMember] + [JsonIgnore] public override bool IsPreSorted => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsDateLastMediaAdded => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => true; - public Guid[] LocalTrailerIds { get; set; } - public Guid[] RemoteTrailerIds { get; set; } + /// <inheritdoc /> + public IReadOnlyList<Guid> LocalTrailerIds { get; set; } + + /// <inheritdoc /> + public IReadOnlyList<Guid> RemoteTrailerIds { get; set; } /// <summary> - /// airdate, dvd or absolute + /// Gets or sets the display order. /// </summary> + /// <remarks> + /// Valid options are airdate, dvd or absolute. + /// </remarks> public string DisplayOrder { get; set; } /// <summary> @@ -60,6 +72,9 @@ namespace MediaBrowser.Controller.Entities.TV /// <value>The status.</value> public SeriesStatus? Status { get; set; } + [JsonIgnore] + public override bool StopRefreshIfLocalMetadataFound => false; + public override double GetDefaultPrimaryImageAspectRatio() { double value = 2; @@ -92,7 +107,7 @@ namespace MediaBrowser.Controller.Entities.TV } var folders = LibraryManager.GetCollectionFolders(this) - .Select(i => i.Id.ToString("N")) + .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) .ToArray(); if (folders.Length == 0) @@ -100,7 +115,7 @@ namespace MediaBrowser.Controller.Entities.TV return key; } - return key + "-" + string.Join("-", folders); + return key + "-" + string.Join('-', folders); } private static string GetUniqueSeriesKey(BaseItem series) @@ -116,7 +131,7 @@ namespace MediaBrowser.Controller.Entities.TV { AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, - IncludeItemTypes = new[] { typeof(Season).Name }, + IncludeItemTypes = new[] { nameof(Season) }, IsVirtualItem = false, Limit = 0, DtoOptions = new DtoOptions(false) @@ -144,8 +159,9 @@ namespace MediaBrowser.Controller.Entities.TV if (query.IncludeItemTypes.Length == 0) { - query.IncludeItemTypes = new[] { typeof(Episode).Name }; + query.IncludeItemTypes = new[] { nameof(Episode) }; } + query.IsVirtualItem = false; query.Limit = 0; var totalRecordCount = LibraryManager.GetCount(query); @@ -161,14 +177,12 @@ namespace MediaBrowser.Controller.Entities.TV { var list = base.GetUserDataKeys(); - var key = this.GetProviderId(MetadataProviders.Imdb); - if (!string.IsNullOrEmpty(key)) + if (this.TryGetProviderId(MetadataProvider.Imdb, out var key)) { list.Insert(0, key); } - key = this.GetProviderId(MetadataProviders.Tvdb); - if (!string.IsNullOrEmpty(key)) + if (this.TryGetProviderId(MetadataProvider.Tvdb, out key)) { list.Insert(0, key); } @@ -199,17 +213,12 @@ namespace MediaBrowser.Controller.Entities.TV query.AncestorWithPresentationUniqueKey = null; query.SeriesPresentationUniqueKey = seriesKey; - query.IncludeItemTypes = new[] { typeof(Season).Name }; - query.OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(); + query.IncludeItemTypes = new[] { nameof(Season) }; + query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }; - if (user != null) + if (user != null && !user.DisplayMissingEpisodes) { - var config = user.Configuration; - - if (!config.DisplayMissingEpisodes) - { - query.IsMissing = false; - } + query.IsMissing = false; } } @@ -223,14 +232,16 @@ namespace MediaBrowser.Controller.Entities.TV query.AncestorWithPresentationUniqueKey = null; query.SeriesPresentationUniqueKey = seriesKey; - if (query.OrderBy.Length == 0) + if (query.OrderBy.Count == 0) { - query.OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(); + query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }; } + if (query.IncludeItemTypes.Length == 0) { - query.IncludeItemTypes = new[] { typeof(Episode).Name, typeof(Season).Name }; + query.IncludeItemTypes = new[] { nameof(Episode), nameof(Season) }; } + query.IsVirtualItem = false; return LibraryManager.GetItemsResult(query); } @@ -248,12 +259,12 @@ namespace MediaBrowser.Controller.Entities.TV { AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, - IncludeItemTypes = new[] { typeof(Episode).Name, typeof(Season).Name }, - OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(), + IncludeItemTypes = new[] { nameof(Episode), nameof(Season) }, + OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, DtoOptions = options }; - var config = user.Configuration; - if (!config.DisplayMissingEpisodes) + + if (!user.DisplayMissingEpisodes) { query.IsMissing = false; } @@ -285,7 +296,7 @@ namespace MediaBrowser.Controller.Entities.TV // Refresh seasons foreach (var item in items) { - if (!(item is Season)) + if (item is not Season) { continue; } @@ -306,27 +317,20 @@ namespace MediaBrowser.Controller.Entities.TV // Refresh episodes and other children foreach (var item in items) { - if ((item is Season)) + if (item is Season) { continue; } cancellationToken.ThrowIfCancellationRequested(); - var skipItem = false; - - var episode = item as Episode; - - if (episode != null + bool skipItem = item is Episode episode && refreshOptions.MetadataRefreshMode != MetadataRefreshMode.FullRefresh && !refreshOptions.ReplaceAllMetadata && episode.IsMissingEpisode && episode.LocationType == LocationType.Virtual && episode.PremiereDate.HasValue - && (DateTime.UtcNow - episode.PremiereDate.Value).TotalDays > 30) - { - skipItem = true; - } + && (DateTime.UtcNow - episode.PremiereDate.Value).TotalDays > 30; if (!skipItem) { @@ -359,14 +363,13 @@ namespace MediaBrowser.Controller.Entities.TV { AncestorWithPresentationUniqueKey = queryFromSeries ? null : seriesKey, SeriesPresentationUniqueKey = queryFromSeries ? seriesKey : null, - IncludeItemTypes = new[] { typeof(Episode).Name }, - OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(), + IncludeItemTypes = new[] { nameof(Episode) }, + OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, DtoOptions = options }; if (user != null) { - var config = user.Configuration; - if (!config.DisplayMissingEpisodes) + if (!user.DisplayMissingEpisodes) { query.IsMissing = false; } @@ -394,6 +397,10 @@ namespace MediaBrowser.Controller.Entities.TV /// <summary> /// Filters the episodes by season. /// </summary> + /// <param name="episodes">The episodes.</param> + /// <param name="parentSeason">The season.</param> + /// <param name="includeSpecials"><c>true</c> to include special, <c>false</c> to not.</param> + /// <returns>The set of episodes.</returns> public static IEnumerable<BaseItem> FilterEpisodesBySeason(IEnumerable<BaseItem> episodes, Season parentSeason, bool includeSpecials) { var seasonNumber = parentSeason.IndexNumber; @@ -424,6 +431,10 @@ namespace MediaBrowser.Controller.Entities.TV /// <summary> /// Filters the episodes by season. /// </summary> + /// <param name="episodes">The episodes.</param> + /// <param name="seasonNumber">The season.</param> + /// <param name="includeSpecials"><c>true</c> to include special, <c>false</c> to not.</param> + /// <returns>The set of episodes.</returns> public static IEnumerable<Episode> FilterEpisodesBySeason(IEnumerable<Episode> episodes, int seasonNumber, bool includeSpecials) { if (!includeSpecials || seasonNumber < 1) @@ -446,10 +457,9 @@ namespace MediaBrowser.Controller.Entities.TV }); } - - protected override bool GetBlockUnratedValue(UserPolicy config) + protected override bool GetBlockUnratedValue(User user) { - return config.BlockUnratedItems.Contains(UnratedItem.Series); + return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Series); } public override UnratedItem GetBlockUnratedType() @@ -488,20 +498,17 @@ namespace MediaBrowser.Controller.Entities.TV { var list = base.GetRelatedUrls(); - var imdbId = this.GetProviderId(MetadataProviders.Imdb); + var imdbId = this.GetProviderId(MetadataProvider.Imdb); if (!string.IsNullOrEmpty(imdbId)) { list.Add(new ExternalUrl { Name = "Trakt", - Url = string.Format("https://trakt.tv/shows/{0}", imdbId) + Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/shows/{0}", imdbId) }); } return list; } - - [IgnoreDataMember] - public override bool StopRefreshIfLocalMetadataFound => false; } } diff --git a/MediaBrowser.Controller/Entities/TagExtensions.cs b/MediaBrowser.Controller/Entities/TagExtensions.cs index 97f590635..2ce396daf 100644 --- a/MediaBrowser.Controller/Entities/TagExtensions.cs +++ b/MediaBrowser.Controller/Entities/TagExtensions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Linq; diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs index 5bf22d7bc..1c558d419 100644 --- a/MediaBrowser.Controller/Entities/Trailer.cs +++ b/MediaBrowser.Controller/Entities/Trailer.cs @@ -1,15 +1,20 @@ +#nullable disable + +#pragma warning disable CA1819, CS1591 + using System; using System.Collections.Generic; +using System.Globalization; +using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { /// <summary> - /// Class Trailer + /// Class Trailer. /// </summary> public class Trailer : Video, IHasLookupInfo<TrailerInfo> { @@ -18,6 +23,9 @@ namespace MediaBrowser.Controller.Entities TrailerTypes = Array.Empty<TrailerType>(); } + [JsonIgnore] + public override bool StopRefreshIfLocalMetadataFound => false; + public TrailerType[] TrailerTypes { get; set; } public override double GetDefaultPrimaryImageAspectRatio() @@ -40,9 +48,9 @@ namespace MediaBrowser.Controller.Entities return info; } - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); if (!ProductionYear.HasValue) { @@ -80,20 +88,17 @@ namespace MediaBrowser.Controller.Entities { var list = base.GetRelatedUrls(); - var imdbId = this.GetProviderId(MetadataProviders.Imdb); + var imdbId = this.GetProviderId(MetadataProvider.Imdb); if (!string.IsNullOrEmpty(imdbId)) { list.Add(new ExternalUrl { Name = "Trakt", - Url = string.Format("https://trakt.tv/movies/{0}", imdbId) + Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/movies/{0}", imdbId) }); } return list; } - - [IgnoreDataMember] - public override bool StopRefreshIfLocalMetadataFound => false; } } diff --git a/MediaBrowser.Controller/Entities/User.cs b/MediaBrowser.Controller/Entities/User.cs deleted file mode 100644 index 0d5f508dd..000000000 --- a/MediaBrowser.Controller/Entities/User.cs +++ /dev/null @@ -1,297 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; - -namespace MediaBrowser.Controller.Entities -{ - /// <summary> - /// Class User - /// </summary> - public class User : BaseItem - { - public static IUserManager UserManager { get; set; } - public static IXmlSerializer XmlSerializer { get; set; } - - /// <summary> - /// From now on all user paths will be Id-based. - /// This is for backwards compatibility. - /// </summary> - public bool UsesIdForConfigurationPath { get; set; } - - /// <summary> - /// Gets or sets the password. - /// </summary> - /// <value>The password.</value> - public string Password { get; set; } - public string EasyPassword { get; set; } - public string Salt { get; set; } - - // Strictly to remove IgnoreDataMember - public override ItemImageInfo[] ImageInfos - { - get => base.ImageInfos; - set => base.ImageInfos = value; - } - - /// <summary> - /// Gets or sets the path. - /// </summary> - /// <value>The path.</value> - [IgnoreDataMember] - public override string Path - { - get => ConfigurationDirectoryPath; - set => base.Path = value; - } - - private string _name; - /// <summary> - /// Gets or sets the name. - /// </summary> - /// <value>The name.</value> - public override string Name - { - get => _name; - set - { - _name = value; - - // lazy load this again - SortName = null; - } - } - - /// <summary> - /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself - /// </summary> - /// <value>The containing folder path.</value> - [IgnoreDataMember] - public override string ContainingFolderPath => Path; - - /// <summary> - /// Gets the root folder. - /// </summary> - /// <value>The root folder.</value> - [IgnoreDataMember] - public Folder RootFolder => LibraryManager.GetUserRootFolder(); - - /// <summary> - /// Gets or sets the last login date. - /// </summary> - /// <value>The last login date.</value> - public DateTime? LastLoginDate { get; set; } - /// <summary> - /// Gets or sets the last activity date. - /// </summary> - /// <value>The last activity date.</value> - public DateTime? LastActivityDate { get; set; } - - private volatile UserConfiguration _config; - private readonly object _configSyncLock = new object(); - [IgnoreDataMember] - public UserConfiguration Configuration - { - get - { - if (_config == null) - { - lock (_configSyncLock) - { - if (_config == null) - { - _config = UserManager.GetUserConfiguration(this); - } - } - } - - return _config; - } - set => _config = value; - } - - private volatile UserPolicy _policy; - private readonly object _policySyncLock = new object(); - [IgnoreDataMember] - public UserPolicy Policy - { - get - { - if (_policy == null) - { - lock (_policySyncLock) - { - if (_policy == null) - { - _policy = UserManager.GetUserPolicy(this); - } - } - } - - return _policy; - } - set => _policy = value; - } - - /// <summary> - /// Renames the user. - /// </summary> - /// <param name="newName">The new name.</param> - /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException"></exception> - public Task Rename(string newName) - { - if (string.IsNullOrEmpty(newName)) - { - throw new ArgumentNullException(nameof(newName)); - } - - // If only the casing is changing, leave the file system alone - if (!UsesIdForConfigurationPath && !string.Equals(newName, Name, StringComparison.OrdinalIgnoreCase)) - { - UsesIdForConfigurationPath = true; - - // Move configuration - var newConfigDirectory = GetConfigurationDirectoryPath(newName); - var oldConfigurationDirectory = ConfigurationDirectoryPath; - - // Exceptions will be thrown if these paths already exist - if (Directory.Exists(newConfigDirectory)) - { - Directory.Delete(newConfigDirectory, true); - } - - if (Directory.Exists(oldConfigurationDirectory)) - { - Directory.Move(oldConfigurationDirectory, newConfigDirectory); - } - else - { - Directory.CreateDirectory(newConfigDirectory); - } - } - - Name = newName; - - return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)) - { - ReplaceAllMetadata = true, - ImageRefreshMode = MetadataRefreshMode.FullRefresh, - MetadataRefreshMode = MetadataRefreshMode.FullRefresh, - ForceSave = true - - }, CancellationToken.None); - } - - public override void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken) - { - UserManager.UpdateUser(this); - } - - /// <summary> - /// Gets the path to the user's configuration directory - /// </summary> - /// <value>The configuration directory path.</value> - [IgnoreDataMember] - public string ConfigurationDirectoryPath => GetConfigurationDirectoryPath(Name); - - public override double GetDefaultPrimaryImageAspectRatio() - { - return 1; - } - - /// <summary> - /// Gets the configuration directory path. - /// </summary> - /// <param name="username">The username.</param> - /// <returns>System.String.</returns> - private string GetConfigurationDirectoryPath(string username) - { - var parentPath = ConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath; - - // Legacy - if (!UsesIdForConfigurationPath) - { - if (string.IsNullOrEmpty(username)) - { - throw new ArgumentNullException(nameof(username)); - } - - var safeFolderName = FileSystem.GetValidFilename(username); - - return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, safeFolderName); - } - - return System.IO.Path.Combine(parentPath, Id.ToString("N")); - } - - public bool IsParentalScheduleAllowed() - { - return IsParentalScheduleAllowed(DateTime.UtcNow); - } - - public bool IsParentalScheduleAllowed(DateTime date) - { - var schedules = Policy.AccessSchedules; - - if (schedules.Length == 0) - { - return true; - } - - foreach (var i in schedules) - { - if (IsParentalScheduleAllowed(i, date)) - { - return true; - } - } - return false; - } - - private bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date) - { - if (date.Kind != DateTimeKind.Utc) - { - throw new ArgumentException("Utc date expected"); - } - - var localTime = date.ToLocalTime(); - - return DayOfWeekHelper.GetDaysOfWeek(schedule.DayOfWeek).Contains(localTime.DayOfWeek) && - IsWithinTime(schedule, localTime); - } - - private bool IsWithinTime(AccessSchedule schedule, DateTime localTime) - { - var hour = localTime.TimeOfDay.TotalHours; - - return hour >= schedule.StartHour && hour <= schedule.EndHour; - } - - public bool IsFolderGrouped(Guid id) - { - foreach (var i in Configuration.GroupedFolders) - { - if (new Guid(i) == id) - { - return true; - } - } - return false; - } - - [IgnoreDataMember] - public override bool SupportsPeople => false; - - public long InternalId { get; set; } - - - } -} diff --git a/MediaBrowser.Controller/Entities/UserItemData.cs b/MediaBrowser.Controller/Entities/UserItemData.cs index f7136bdf2..50ba9ef30 100644 --- a/MediaBrowser.Controller/Entities/UserItemData.cs +++ b/MediaBrowser.Controller/Entities/UserItemData.cs @@ -1,13 +1,24 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; -using MediaBrowser.Model.Serialization; +using System.Text.Json.Serialization; namespace MediaBrowser.Controller.Entities { /// <summary> - /// Class UserItemData + /// Class UserItemData. /// </summary> public class UserItemData { + public const double MinLikeValue = 6.5; + + /// <summary> + /// The _rating. + /// </summary> + private double? _rating; + /// <summary> /// Gets or sets the user id. /// </summary> @@ -21,11 +32,7 @@ namespace MediaBrowser.Controller.Entities public string Key { get; set; } /// <summary> - /// The _rating - /// </summary> - private double? _rating; - /// <summary> - /// Gets or sets the users 0-10 rating + /// Gets or sets the users 0-10 rating. /// </summary> /// <value>The rating.</value> /// <exception cref="ArgumentOutOfRangeException">Rating;A 0 to 10 rating is required for UserItemData.</exception> @@ -75,25 +82,25 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <value><c>true</c> if played; otherwise, <c>false</c>.</value> public bool Played { get; set; } + /// <summary> /// Gets or sets the index of the audio stream. /// </summary> /// <value>The index of the audio stream.</value> public int? AudioStreamIndex { get; set; } + /// <summary> /// Gets or sets the index of the subtitle stream. /// </summary> /// <value>The index of the subtitle stream.</value> public int? SubtitleStreamIndex { get; set; } - public const double MinLikeValue = 6.5; - /// <summary> - /// This is an interpreted property to indicate likes or dislikes + /// Gets or sets a value indicating whether the item is liked or not. /// This should never be serialized. /// </summary> /// <value><c>null</c> if [likes] contains no value, <c>true</c> if [likes]; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool? Likes { get @@ -105,6 +112,7 @@ namespace MediaBrowser.Controller.Entities return null; } + set { if (value.HasValue) diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs index 7fe8df8af..e547db523 100644 --- a/MediaBrowser.Controller/Entities/UserRootFolder.cs +++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs @@ -1,12 +1,17 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { @@ -16,29 +21,29 @@ namespace MediaBrowser.Controller.Entities /// </summary> public class UserRootFolder : Folder { - private List<Guid> _childrenIds = null; private readonly object _childIdsLock = new object(); - protected override List<BaseItem> LoadChildren() - { - lock (_childIdsLock) - { - if (_childrenIds == null) - { - var list = base.LoadChildren(); - _childrenIds = list.Select(i => i.Id).ToList(); - return list; - } + private List<Guid> _childrenIds = null; - return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList(); - } + /// <summary> + /// Initializes a new instance of the <see cref="UserRootFolder"/> class. + /// </summary> + public UserRootFolder() + { + IsRoot = true; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => false; + [JsonIgnore] + protected override bool SupportsShortcutChildren => true; + + [JsonIgnore] + public override bool IsPreSorted => true; + private void ClearCache() { lock (_childIdsLock) @@ -47,6 +52,21 @@ namespace MediaBrowser.Controller.Entities } } + protected override List<BaseItem> LoadChildren() + { + lock (_childIdsLock) + { + if (_childrenIds == null) + { + var list = base.LoadChildren(); + _childrenIds = list.Select(i => i.Id).ToList(); + return list; + } + + return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList(); + } + } + protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) { if (query.Recursive) @@ -60,14 +80,7 @@ namespace MediaBrowser.Controller.Entities PresetViews = query.PresetViews }); - var itemsArray = result; - var totalCount = itemsArray.Length; - - return new QueryResult<BaseItem> - { - TotalRecordCount = totalCount, - Items = itemsArray //TODO Fix The co-variant conversion between Folder[] and BaseItem[], this can generate runtime issues. - }; + return UserViewBuilder.SortAndPage(result, null, query, LibraryManager, true); } public override int GetChildCount(User user) @@ -75,12 +88,6 @@ namespace MediaBrowser.Controller.Entities return GetChildren(user, true).Count; } - [IgnoreDataMember] - protected override bool SupportsShortcutChildren => true; - - [IgnoreDataMember] - public override bool IsPreSorted => true; - protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) { var list = base.GetEligibleChildrenForRecursiveChildren(user).ToList(); @@ -89,10 +96,10 @@ namespace MediaBrowser.Controller.Entities return list; } - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { ClearCache(); - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); if (string.Equals("default", Name, StringComparison.OrdinalIgnoreCase)) { @@ -110,11 +117,11 @@ namespace MediaBrowser.Controller.Entities return base.GetNonCachedChildren(directoryService); } - protected override async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + protected override async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { ClearCache(); - await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService) + await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken) .ConfigureAwait(false); ClearCache(); diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index 3e2191376..62f3c4b55 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -1,57 +1,94 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using System.Threading.Tasks; -using MediaBrowser.Controller.Playlists; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.TV; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { public class UserView : Folder, IHasCollectionType { + private static readonly string[] _viewTypesEligibleForGrouping = new string[] + { + Model.Entities.CollectionType.Movies, + Model.Entities.CollectionType.TvShows, + string.Empty + }; + + private static readonly string[] _originalFolderViewTypes = new string[] + { + Model.Entities.CollectionType.Books, + Model.Entities.CollectionType.MusicVideos, + Model.Entities.CollectionType.HomeVideos, + Model.Entities.CollectionType.Photos, + Model.Entities.CollectionType.Music, + Model.Entities.CollectionType.BoxSets + }; + + public static ITVSeriesManager TVSeriesManager { get; set; } + + /// <summary> + /// Gets or sets the view type. + /// </summary> public string ViewType { get; set; } + + /// <summary> + /// Gets or sets the display parent id. + /// </summary> public new Guid DisplayParentId { get; set; } + /// <summary> + /// Gets or sets the user id. + /// </summary> public Guid? UserId { get; set; } - public static ITVSeriesManager TVSeriesManager; - public static IPlaylistManager PlaylistManager; - - [IgnoreDataMember] + /// <inheritdoc /> + [JsonIgnore] public string CollectionType => ViewType; + /// <inheritdoc /> + [JsonIgnore] + public override bool SupportsInheritedParentImages => false; + + /// <inheritdoc /> + [JsonIgnore] + public override bool SupportsPlayedStatus => false; + + /// <inheritdoc /> + [JsonIgnore] + public override bool SupportsPeople => false; + + /// <inheritdoc /> public override IEnumerable<Guid> GetIdsForAncestorQuery() { - var list = new List<Guid>(); - if (!DisplayParentId.Equals(Guid.Empty)) { - list.Add(DisplayParentId); + yield return DisplayParentId; } else if (!ParentId.Equals(Guid.Empty)) { - list.Add(ParentId); + yield return ParentId; } else { - list.Add(Id); + yield return Id; } - return list; } - [IgnoreDataMember] - public override bool SupportsInheritedParentImages => false; - - [IgnoreDataMember] - public override bool SupportsPlayedStatus => false; - + /// <inheritdoc /> public override int GetChildCount(User user) { return GetChildren(user, true).Count; } + /// <inheritdoc /> protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) { var parent = this as Folder; @@ -65,16 +102,14 @@ namespace MediaBrowser.Controller.Entities parent = LibraryManager.GetItemById(ParentId) as Folder ?? parent; } - return new UserViewBuilder(UserViewManager, LibraryManager, Logger, UserDataManager, TVSeriesManager, ConfigurationManager, PlaylistManager) + return new UserViewBuilder(UserViewManager, LibraryManager, Logger, UserDataManager, TVSeriesManager, ConfigurationManager) .GetUserItems(parent, this, CollectionType, query); } + /// <inheritdoc /> public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) { - if (query == null) - { - query = new InternalItemsQuery(user); - } + query ??= new InternalItemsQuery(user); query.EnableTotalRecordCount = false; var result = GetItemList(query); @@ -82,16 +117,19 @@ namespace MediaBrowser.Controller.Entities return result.ToList(); } + /// <inheritdoc /> public override bool CanDelete() { return false; } + /// <inheritdoc /> public override bool IsSaveLocalMetadataEnabled() { return true; } + /// <inheritdoc /> public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) { query.SetUser(user); @@ -102,32 +140,26 @@ namespace MediaBrowser.Controller.Entities return GetItemList(query); } + /// <inheritdoc /> protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) { return GetChildren(user, false); } - private static string[] UserSpecificViewTypes = new string[] - { - MediaBrowser.Model.Entities.CollectionType.Playlists - }; - public static bool IsUserSpecific(Folder folder) { - var collectionFolder = folder as ICollectionFolder; - - if (collectionFolder == null) + if (folder is not ICollectionFolder collectionFolder) { return false; } - var supportsUserSpecific = folder as ISupportsUserSpecificView; - if (supportsUserSpecific != null && supportsUserSpecific.EnableUserSpecificView) + if (folder is ISupportsUserSpecificView supportsUserSpecific + && supportsUserSpecific.EnableUserSpecificView) { return true; } - return UserSpecificViewTypes.Contains(collectionFolder.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + return string.Equals(Model.Entities.CollectionType.Playlists, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase); } public static bool IsEligibleForGrouping(Folder folder) @@ -136,39 +168,19 @@ namespace MediaBrowser.Controller.Entities && IsEligibleForGrouping(collectionFolder.CollectionType); } - private static string[] ViewTypesEligibleForGrouping = new string[] - { - MediaBrowser.Model.Entities.CollectionType.Movies, - MediaBrowser.Model.Entities.CollectionType.TvShows, - string.Empty - }; - public static bool IsEligibleForGrouping(string viewType) { - return ViewTypesEligibleForGrouping.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + return _viewTypesEligibleForGrouping.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); } - private static string[] OriginalFolderViewTypes = new string[] - { - MediaBrowser.Model.Entities.CollectionType.Books, - MediaBrowser.Model.Entities.CollectionType.MusicVideos, - MediaBrowser.Model.Entities.CollectionType.HomeVideos, - MediaBrowser.Model.Entities.CollectionType.Photos, - MediaBrowser.Model.Entities.CollectionType.Music, - MediaBrowser.Model.Entities.CollectionType.BoxSets - }; - public static bool EnableOriginalFolder(string viewType) { - return OriginalFolderViewTypes.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + return _originalFolderViewTypes.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); } - protected override Task ValidateChildrenInternal(IProgress<double> progress, System.Threading.CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, Providers.MetadataRefreshOptions refreshOptions, Providers.IDirectoryService directoryService) + protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, Providers.MetadataRefreshOptions refreshOptions, Providers.IDirectoryService directoryService, System.Threading.CancellationToken cancellationToken) { return Task.CompletedTask; } - - [IgnoreDataMember] - public override bool SupportsPeople => false; } } diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 683218a9e..266fda767 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -1,15 +1,25 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.TV; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using Microsoft.Extensions.Logging; +using Episode = MediaBrowser.Controller.Entities.TV.Episode; +using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider; +using Movie = MediaBrowser.Controller.Entities.Movies.Movie; +using Season = MediaBrowser.Controller.Entities.TV.Season; +using Series = MediaBrowser.Controller.Entities.TV.Series; namespace MediaBrowser.Controller.Entities { @@ -17,13 +27,18 @@ namespace MediaBrowser.Controller.Entities { private readonly IUserViewManager _userViewManager; private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; + private readonly ILogger<BaseItem> _logger; private readonly IUserDataManager _userDataManager; private readonly ITVSeriesManager _tvSeriesManager; private readonly IServerConfigurationManager _config; - private readonly IPlaylistManager _playlistManager; - public UserViewBuilder(IUserViewManager userViewManager, ILibraryManager libraryManager, ILogger logger, IUserDataManager userDataManager, ITVSeriesManager tvSeriesManager, IServerConfigurationManager config, IPlaylistManager playlistManager) + public UserViewBuilder( + IUserViewManager userViewManager, + ILibraryManager libraryManager, + ILogger<BaseItem> logger, + IUserDataManager userDataManager, + ITVSeriesManager tvSeriesManager, + IServerConfigurationManager config) { _userViewManager = userViewManager; _libraryManager = libraryManager; @@ -31,27 +46,26 @@ namespace MediaBrowser.Controller.Entities _userDataManager = userDataManager; _tvSeriesManager = tvSeriesManager; _config = config; - _playlistManager = playlistManager; } public QueryResult<BaseItem> GetUserItems(Folder queryParent, Folder displayParent, string viewType, InternalItemsQuery query) { var user = query.User; - //if (query.IncludeItemTypes != null && + // if (query.IncludeItemTypes != null && // query.IncludeItemTypes.Length == 1 && // string.Equals(query.IncludeItemTypes[0], "Playlist", StringComparison.OrdinalIgnoreCase)) - //{ + // { // if (!string.Equals(viewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) // { // return await FindPlaylists(queryParent, user, query).ConfigureAwait(false); // } - //} + // } switch (viewType) { case CollectionType.Folders: - return GetResult(_libraryManager.GetUserRootFolder().GetChildren(user, true), queryParent, query); + return GetResult(_libraryManager.GetUserRootFolder().GetChildren(user, true), query); case CollectionType.TvShows: return GetTvView(queryParent, user, query); @@ -96,7 +110,7 @@ namespace MediaBrowser.Controller.Entities return GetMovieMovies(queryParent, user, query); case SpecialFolder.MovieCollections: - return GetMovieCollections(queryParent, user, query); + return GetMovieCollections(user, query); case SpecialFolder.TvFavoriteEpisodes: return GetFavoriteEpisodes(queryParent, user, query); @@ -108,8 +122,9 @@ namespace MediaBrowser.Controller.Entities { if (queryParent is UserView) { - return GetResult(GetMediaFolders(user).OfType<Folder>().SelectMany(i => i.GetChildren(user, true)), queryParent, query); + return GetResult(GetMediaFolders(user).OfType<Folder>().SelectMany(i => i.GetChildren(user, true)), query); } + return queryParent.GetItems(query); } } @@ -129,22 +144,23 @@ namespace MediaBrowser.Controller.Entities if (query.IncludeItemTypes.Length == 0) { - query.IncludeItemTypes = new[] { typeof(Movie).Name }; + query.IncludeItemTypes = new[] { nameof(Movie) }; } return parent.QueryRecursive(query); } - var list = new List<BaseItem>(); - - list.Add(GetUserView(SpecialFolder.MovieResume, "HeaderContinueWatching", "0", parent)); - list.Add(GetUserView(SpecialFolder.MovieLatest, "Latest", "1", parent)); - list.Add(GetUserView(SpecialFolder.MovieMovies, "Movies", "2", parent)); - list.Add(GetUserView(SpecialFolder.MovieCollections, "Collections", "3", parent)); - list.Add(GetUserView(SpecialFolder.MovieFavorites, "Favorites", "4", parent)); - list.Add(GetUserView(SpecialFolder.MovieGenres, "Genres", "5", parent)); + var list = new List<BaseItem> + { + GetUserView(SpecialFolder.MovieResume, "HeaderContinueWatching", "0", parent), + GetUserView(SpecialFolder.MovieLatest, "Latest", "1", parent), + GetUserView(SpecialFolder.MovieMovies, "Movies", "2", parent), + GetUserView(SpecialFolder.MovieCollections, "Collections", "3", parent), + GetUserView(SpecialFolder.MovieFavorites, "Favorites", "4", parent), + GetUserView(SpecialFolder.MovieGenres, "Genres", "5", parent) + }; - return GetResult(list, parent, query); + return GetResult(list, query); } private QueryResult<BaseItem> GetFavoriteMovies(Folder parent, User user, InternalItemsQuery query) @@ -153,7 +169,7 @@ namespace MediaBrowser.Controller.Entities query.Parent = parent; query.SetUser(user); query.IsFavorite = true; - query.IncludeItemTypes = new[] { typeof(Movie).Name }; + query.IncludeItemTypes = new[] { nameof(Movie) }; return _libraryManager.GetItemsResult(query); } @@ -164,7 +180,7 @@ namespace MediaBrowser.Controller.Entities query.Parent = parent; query.SetUser(user); query.IsFavorite = true; - query.IncludeItemTypes = new[] { typeof(Series).Name }; + query.IncludeItemTypes = new[] { nameof(Series) }; return _libraryManager.GetItemsResult(query); } @@ -175,7 +191,7 @@ namespace MediaBrowser.Controller.Entities query.Parent = parent; query.SetUser(user); query.IsFavorite = true; - query.IncludeItemTypes = new[] { typeof(Episode).Name }; + query.IncludeItemTypes = new[] { nameof(Episode) }; return _libraryManager.GetItemsResult(query); } @@ -186,15 +202,15 @@ namespace MediaBrowser.Controller.Entities query.Parent = parent; query.SetUser(user); - query.IncludeItemTypes = new[] { typeof(Movie).Name }; + query.IncludeItemTypes = new[] { nameof(Movie) }; return _libraryManager.GetItemsResult(query); } - private QueryResult<BaseItem> GetMovieCollections(Folder parent, User user, InternalItemsQuery query) + private QueryResult<BaseItem> GetMovieCollections(User user, InternalItemsQuery query) { query.Parent = null; - query.IncludeItemTypes = new[] { typeof(BoxSet).Name }; + query.IncludeItemTypes = new[] { nameof(BoxSet) }; query.SetUser(user); query.Recursive = true; @@ -203,26 +219,25 @@ namespace MediaBrowser.Controller.Entities private QueryResult<BaseItem> GetMovieLatest(Folder parent, User user, InternalItemsQuery query) { - query.OrderBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(); - + query.OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) }; query.Recursive = true; query.Parent = parent; query.SetUser(user); query.Limit = GetSpecialItemsLimit(); - query.IncludeItemTypes = new[] { typeof(Movie).Name }; + query.IncludeItemTypes = new[] { nameof(Movie) }; return ConvertToResult(_libraryManager.GetItemList(query)); } private QueryResult<BaseItem> GetMovieResume(Folder parent, User user, InternalItemsQuery query) { - query.OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(); + query.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) }; query.IsResumable = true; query.Recursive = true; query.Parent = parent; query.SetUser(user); query.Limit = GetSpecialItemsLimit(); - query.IncludeItemTypes = new[] { typeof(Movie).Name }; + query.IncludeItemTypes = new[] { nameof(Movie) }; return ConvertToResult(_libraryManager.GetItemList(query)); } @@ -241,10 +256,9 @@ namespace MediaBrowser.Controller.Entities { var genres = parent.QueryRecursive(new InternalItemsQuery(user) { - IncludeItemTypes = new[] { typeof(Movie).Name }, + IncludeItemTypes = new[] { nameof(Movie) }, Recursive = true, EnableTotalRecordCount = false - }).Items .SelectMany(i => i.Genres) .DistinctNames() @@ -259,12 +273,11 @@ namespace MediaBrowser.Controller.Entities _logger.LogError(ex, "Error getting genre"); return null; } - }) .Where(i => i != null) - .Select(i => GetUserViewWithName(i.Name, SpecialFolder.MovieGenre, i.SortName, parent)); + .Select(i => GetUserViewWithName(SpecialFolder.MovieGenre, i.SortName, parent)); - return GetResult(genres, parent, query); + return GetResult(genres, query); } private QueryResult<BaseItem> GetMovieGenreItems(Folder queryParent, Folder displayParent, User user, InternalItemsQuery query) @@ -274,7 +287,7 @@ namespace MediaBrowser.Controller.Entities query.GenreIds = new[] { displayParent.Id }; query.SetUser(user); - query.IncludeItemTypes = new[] { typeof(Movie).Name }; + query.IncludeItemTypes = new[] { nameof(Movie) }; return _libraryManager.GetItemsResult(query); } @@ -288,34 +301,39 @@ namespace MediaBrowser.Controller.Entities if (query.IncludeItemTypes.Length == 0) { - query.IncludeItemTypes = new[] { typeof(Series).Name, typeof(Season).Name, typeof(Episode).Name }; + query.IncludeItemTypes = new[] + { + nameof(Series), + nameof(Season), + nameof(Episode) + }; } return parent.QueryRecursive(query); } - var list = new List<BaseItem>(); - - list.Add(GetUserView(SpecialFolder.TvResume, "HeaderContinueWatching", "0", parent)); - list.Add(GetUserView(SpecialFolder.TvNextUp, "HeaderNextUp", "1", parent)); - list.Add(GetUserView(SpecialFolder.TvLatest, "Latest", "2", parent)); - list.Add(GetUserView(SpecialFolder.TvShowSeries, "Shows", "3", parent)); - list.Add(GetUserView(SpecialFolder.TvFavoriteSeries, "HeaderFavoriteShows", "4", parent)); - list.Add(GetUserView(SpecialFolder.TvFavoriteEpisodes, "HeaderFavoriteEpisodes", "5", parent)); - list.Add(GetUserView(SpecialFolder.TvGenres, "Genres", "6", parent)); + var list = new List<BaseItem> + { + GetUserView(SpecialFolder.TvResume, "HeaderContinueWatching", "0", parent), + GetUserView(SpecialFolder.TvNextUp, "HeaderNextUp", "1", parent), + GetUserView(SpecialFolder.TvLatest, "Latest", "2", parent), + GetUserView(SpecialFolder.TvShowSeries, "Shows", "3", parent), + GetUserView(SpecialFolder.TvFavoriteSeries, "HeaderFavoriteShows", "4", parent), + GetUserView(SpecialFolder.TvFavoriteEpisodes, "HeaderFavoriteEpisodes", "5", parent), + GetUserView(SpecialFolder.TvGenres, "Genres", "6", parent) + }; - return GetResult(list, parent, query); + return GetResult(list, query); } private QueryResult<BaseItem> GetTvLatest(Folder parent, User user, InternalItemsQuery query) { - query.OrderBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(); - + query.OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) }; query.Recursive = true; query.Parent = parent; query.SetUser(user); query.Limit = GetSpecialItemsLimit(); - query.IncludeItemTypes = new[] { typeof(Episode).Name }; + query.IncludeItemTypes = new[] { nameof(Episode) }; query.IsVirtualItem = false; return ConvertToResult(_libraryManager.GetItemList(query)); @@ -325,26 +343,28 @@ namespace MediaBrowser.Controller.Entities { var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.TvShows, string.Empty }); - var result = _tvSeriesManager.GetNextUp(new NextUpQuery - { - Limit = query.Limit, - StartIndex = query.StartIndex, - UserId = query.User.Id - - }, parentFolders, query.DtoOptions); + var result = _tvSeriesManager.GetNextUp( + new NextUpQuery + { + Limit = query.Limit, + StartIndex = query.StartIndex, + UserId = query.User.Id + }, + parentFolders, + query.DtoOptions); return result; } private QueryResult<BaseItem> GetTvResume(Folder parent, User user, InternalItemsQuery query) { - query.OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(); + query.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) }; query.IsResumable = true; query.Recursive = true; query.Parent = parent; query.SetUser(user); query.Limit = GetSpecialItemsLimit(); - query.IncludeItemTypes = new[] { typeof(Episode).Name }; + query.IncludeItemTypes = new[] { nameof(Episode) }; return ConvertToResult(_libraryManager.GetItemList(query)); } @@ -355,7 +375,7 @@ namespace MediaBrowser.Controller.Entities query.Parent = parent; query.SetUser(user); - query.IncludeItemTypes = new[] { typeof(Series).Name }; + query.IncludeItemTypes = new[] { nameof(Series) }; return _libraryManager.GetItemsResult(query); } @@ -364,10 +384,9 @@ namespace MediaBrowser.Controller.Entities { var genres = parent.QueryRecursive(new InternalItemsQuery(user) { - IncludeItemTypes = new[] { typeof(Series).Name }, + IncludeItemTypes = new[] { nameof(Series) }, Recursive = true, EnableTotalRecordCount = false - }).Items .SelectMany(i => i.Genres) .DistinctNames() @@ -382,12 +401,11 @@ namespace MediaBrowser.Controller.Entities _logger.LogError(ex, "Error getting genre"); return null; } - }) .Where(i => i != null) - .Select(i => GetUserViewWithName(i.Name, SpecialFolder.TvGenre, i.SortName, parent)); + .Select(i => GetUserViewWithName(SpecialFolder.TvGenre, i.SortName, parent)); - return GetResult(genres, parent, query); + return GetResult(genres, query); } private QueryResult<BaseItem> GetTvGenreItems(Folder queryParent, Folder displayParent, User user, InternalItemsQuery query) @@ -397,7 +415,7 @@ namespace MediaBrowser.Controller.Entities query.GenreIds = new[] { displayParent.Id }; query.SetUser(user); - query.IncludeItemTypes = new[] { typeof(Series).Name }; + query.IncludeItemTypes = new[] { nameof(Series) }; return _libraryManager.GetItemsResult(query); } @@ -407,19 +425,19 @@ namespace MediaBrowser.Controller.Entities { return new QueryResult<BaseItem> { - Items = result.Items, //TODO Fix The co-variant conversion between T[] and BaseItem[], this can generate runtime issues if T is not BaseItem. + Items = result.Items, // TODO Fix The co-variant conversion between T[] and BaseItem[], this can generate runtime issues if T is not BaseItem. TotalRecordCount = result.TotalRecordCount }; } - private QueryResult<BaseItem> GetResult<T>(IEnumerable<T> items, - BaseItem queryParent, + private QueryResult<BaseItem> GetResult<T>( + IEnumerable<T> items, InternalItemsQuery query) where T : BaseItem { items = items.Where(i => Filter(i, query.User, query, _userDataManager, _libraryManager)); - return PostFilterAndSort(items, queryParent, null, query, _libraryManager, _config); + return PostFilterAndSort(items, null, query, _libraryManager); } public static bool FilterItem(BaseItem item, InternalItemsQuery query) @@ -427,12 +445,11 @@ namespace MediaBrowser.Controller.Entities return Filter(item, query.User, query, BaseItem.UserDataManager, BaseItem.LibraryManager); } - public static QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, - BaseItem queryParent, + public static QueryResult<BaseItem> PostFilterAndSort( + IEnumerable<BaseItem> items, int? totalRecordLimit, InternalItemsQuery query, - ILibraryManager libraryManager, - IServerConfigurationManager configurationManager) + ILibraryManager libraryManager) { var user = query.User; @@ -445,14 +462,16 @@ namespace MediaBrowser.Controller.Entities return SortAndPage(items, totalRecordLimit, query, libraryManager, true); } - public static QueryResult<BaseItem> SortAndPage(IEnumerable<BaseItem> items, + public static QueryResult<BaseItem> SortAndPage( + IEnumerable<BaseItem> items, int? totalRecordLimit, InternalItemsQuery query, - ILibraryManager libraryManager, bool enableSorting) + ILibraryManager libraryManager, + bool enableSorting) { if (enableSorting) { - if (query.OrderBy.Length > 0) + if (query.OrderBy.Count > 0) { items = libraryManager.Sort(items, query.User, query.OrderBy); } @@ -604,7 +623,7 @@ namespace MediaBrowser.Controller.Entities { var filterValue = query.HasImdbId.Value; - var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Imdb)); + var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProvider.Imdb)); if (hasValue != filterValue) { @@ -616,7 +635,7 @@ namespace MediaBrowser.Controller.Entities { var filterValue = query.HasTmdbId.Value; - var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Tmdb)); + var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProvider.Tmdb)); if (hasValue != filterValue) { @@ -628,7 +647,7 @@ namespace MediaBrowser.Controller.Entities { var filterValue = query.HasTvdbId.Value; - var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Tvdb)); + var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProvider.Tvdb)); if (hasValue != filterValue) { @@ -654,9 +673,7 @@ namespace MediaBrowser.Controller.Entities var isPlaceHolder = false; - var hasPlaceHolder = item as ISupportsPlaceHolders; - - if (hasPlaceHolder != null) + if (item is ISupportsPlaceHolders hasPlaceHolder) { isPlaceHolder = hasPlaceHolder.IsPlaceHolder; } @@ -671,13 +688,11 @@ namespace MediaBrowser.Controller.Entities { var filterValue = query.HasSpecialFeature.Value; - var movie = item as IHasSpecialFeatures; - - if (movie != null) + if (item is IHasSpecialFeatures movie) { var ok = filterValue - ? movie.SpecialFeatureIds.Length > 0 - : movie.SpecialFeatureIds.Length == 0; + ? movie.SpecialFeatureIds.Count > 0 + : movie.SpecialFeatureIds.Count == 0; if (!ok) { @@ -775,7 +790,7 @@ namespace MediaBrowser.Controller.Entities } // Apply genre filter - if (query.Genres.Length > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparer.OrdinalIgnoreCase))) + if (query.Genres.Count > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparer.OrdinalIgnoreCase))) { return false; } @@ -806,7 +821,7 @@ namespace MediaBrowser.Controller.Entities } // Apply genre filter - if (query.GenreIds.Length > 0 && !query.GenreIds.Any(id => + if (query.GenreIds.Count > 0 && !query.GenreIds.Any(id => { var genreItem = libraryManager.GetItemById(id); return genreItem != null && item.Genres.Contains(genreItem.Name, StringComparer.OrdinalIgnoreCase); @@ -944,6 +959,7 @@ namespace MediaBrowser.Controller.Entities .OfType<Folder>() .Where(UserView.IsEligibleForGrouping); } + return _libraryManager.GetUserRootFolder() .GetChildren(user, true) .OfType<Folder>() @@ -962,6 +978,7 @@ namespace MediaBrowser.Controller.Entities return folder != null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase); }).ToArray(); } + return GetMediaFolders(user) .Where(i => { @@ -981,9 +998,9 @@ namespace MediaBrowser.Controller.Entities return new BaseItem[] { parent }; } - private UserView GetUserViewWithName(string name, string type, string sortName, BaseItem parent) + private UserView GetUserViewWithName(string type, string sortName, BaseItem parent) { - return _userViewManager.GetUserSubView(parent.Id, parent.Id.ToString("N"), type, sortName); + return _userViewManager.GetUserSubView(parent.Id, parent.Id.ToString("N", CultureInfo.InvariantCulture), type, sortName); } private UserView GetUserView(string type, string localizationKey, string sortName, BaseItem parent) diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 31cd42975..7dd95b85c 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -1,47 +1,60 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { /// <summary> - /// Class Video + /// Class Video. /// </summary> public class Video : BaseItem, IHasAspectRatio, ISupportsPlaceHolders, IHasMediaSources { - [IgnoreDataMember] + public Video() + { + AdditionalParts = Array.Empty<string>(); + LocalAlternateVersions = Array.Empty<string>(); + SubtitleFiles = Array.Empty<string>(); + LinkedAlternateVersions = Array.Empty<LinkedChild>(); + } + + [JsonIgnore] public string PrimaryVersionId { get; set; } public string[] AdditionalParts { get; set; } + public string[] LocalAlternateVersions { get; set; } + public LinkedChild[] LinkedAlternateVersions { get; set; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPositionTicksResume { get @@ -53,44 +66,23 @@ namespace MediaBrowser.Controller.Entities { return false; } + if (extraType.Value == Model.Entities.ExtraType.ThemeVideo) { return false; } + if (extraType.Value == Model.Entities.ExtraType.Trailer) { return false; } } - return true; - } - } - public void SetPrimaryVersionId(string id) - { - if (string.IsNullOrEmpty(id)) - { - PrimaryVersionId = null; - } - else - { - PrimaryVersionId = id; - } - - PresentationUniqueKey = CreatePresentationUniqueKey(); - } - - public override string CreatePresentationUniqueKey() - { - if (!string.IsNullOrEmpty(PrimaryVersionId)) - { - return PrimaryVersionId; + return true; } - - return base.CreatePresentationUniqueKey(); } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsThemeMedia => true; /// <summary> @@ -137,53 +129,16 @@ namespace MediaBrowser.Controller.Entities /// <value>The video3 D format.</value> public Video3DFormat? Video3DFormat { get; set; } - public string[] GetPlayableStreamFileNames(IMediaEncoder mediaEncoder) - { - var videoType = VideoType; - - if (videoType == VideoType.Iso && IsoType == Model.Entities.IsoType.BluRay) - { - videoType = VideoType.BluRay; - } - else if (videoType == VideoType.Iso && IsoType == Model.Entities.IsoType.Dvd) - { - videoType = VideoType.Dvd; - } - else - { - return Array.Empty<string>(); - } - return mediaEncoder.GetPlayableStreamFileNames(Path, videoType); - } - /// <summary> /// Gets or sets the aspect ratio. /// </summary> /// <value>The aspect ratio.</value> public string AspectRatio { get; set; } - public Video() - { - AdditionalParts = Array.Empty<string>(); - LocalAlternateVersions = Array.Empty<string>(); - SubtitleFiles = Array.Empty<string>(); - LinkedAlternateVersions = EmptyLinkedChildArray; - } - - public override bool CanDownload() - { - if (VideoType == VideoType.Dvd || VideoType == VideoType.BluRay) - { - return false; - } - - return IsFileProtocol; - } - - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsAddingToPlaylist => true; - [IgnoreDataMember] + [JsonIgnore] public int MediaSourceCount { get @@ -196,29 +151,20 @@ namespace MediaBrowser.Controller.Entities return video.MediaSourceCount; } } + return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1; } } - [IgnoreDataMember] + [JsonIgnore] public bool IsStacked => AdditionalParts.Length > 0; - [IgnoreDataMember] + [JsonIgnore] public override bool HasLocalAlternateVersions => LocalAlternateVersions.Length > 0; - public IEnumerable<Guid> GetAdditionalPartIds() - { - return AdditionalParts.Select(i => LibraryManager.GetNewItemId(i, typeof(Video))); - } - - public IEnumerable<Guid> GetLocalAlternateVersionIds() - { - return LocalAlternateVersions.Select(i => LibraryManager.GetNewItemId(i, typeof(Video))); - } - public static ILiveTvManager LiveTvManager { get; set; } - [IgnoreDataMember] + [JsonIgnore] public override SourceType SourceType { get @@ -232,37 +178,77 @@ namespace MediaBrowser.Controller.Entities } } - protected override bool IsActiveRecording() + [JsonIgnore] + public bool IsCompleteMedia { - return LiveTvManager.GetActiveRecordingInfo(Path) != null; + get + { + if (SourceType == SourceType.Channel) + { + return !Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase); + } + + return !IsActiveRecording(); + } } - public override bool CanDelete() + [JsonIgnore] + protected virtual bool EnableDefaultVideoUserDataKeys => true; + + [JsonIgnore] + public override string ContainingFolderPath { - if (IsActiveRecording()) + get { - return false; - } + if (IsStacked) + { + return System.IO.Path.GetDirectoryName(Path); + } - return base.CanDelete(); + if (!IsPlaceHolder) + { + if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd) + { + return Path; + } + } + + return base.ContainingFolderPath; + } } - [IgnoreDataMember] - public bool IsCompleteMedia + [JsonIgnore] + public override string FileNameWithoutExtension { get { - if (SourceType == SourceType.Channel) + if (IsFileProtocol) { - return !Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase); + if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd) + { + return System.IO.Path.GetFileName(Path); + } + + return System.IO.Path.GetFileNameWithoutExtension(Path); } - return !IsActiveRecording(); + return null; } } - [IgnoreDataMember] - protected virtual bool EnableDefaultVideoUserDataKeys => true; + /// <summary> + /// Gets a value indicating whether [is3 D]. + /// </summary> + /// <value><c>true</c> if [is3 D]; otherwise, <c>false</c>.</value> + [JsonIgnore] + public bool Is3D => Video3DFormat.HasValue; + + /// <summary> + /// Gets the type of the media. + /// </summary> + /// <value>The type of the media.</value> + [JsonIgnore] + public override string MediaType => Model.Entities.MediaType.Video; public override List<string> GetUserDataKeys() { @@ -272,13 +258,13 @@ namespace MediaBrowser.Controller.Entities { if (ExtraType.HasValue) { - var key = this.GetProviderId(MetadataProviders.Tmdb); + var key = this.GetProviderId(MetadataProvider.Tmdb); if (!string.IsNullOrEmpty(key)) { list.Insert(0, GetUserDataKey(key)); } - key = this.GetProviderId(MetadataProviders.Imdb); + key = this.GetProviderId(MetadataProvider.Imdb); if (!string.IsNullOrEmpty(key)) { list.Insert(0, GetUserDataKey(key)); @@ -286,13 +272,13 @@ namespace MediaBrowser.Controller.Entities } else { - var key = this.GetProviderId(MetadataProviders.Imdb); + var key = this.GetProviderId(MetadataProvider.Imdb); if (!string.IsNullOrEmpty(key)) { list.Insert(0, key); } - key = this.GetProviderId(MetadataProviders.Tmdb); + key = this.GetProviderId(MetadataProvider.Tmdb); if (!string.IsNullOrEmpty(key)) { list.Insert(0, key); @@ -303,6 +289,65 @@ namespace MediaBrowser.Controller.Entities return list; } + public void SetPrimaryVersionId(string id) + { + if (string.IsNullOrEmpty(id)) + { + PrimaryVersionId = null; + } + else + { + PrimaryVersionId = id; + } + + PresentationUniqueKey = CreatePresentationUniqueKey(); + } + + public override string CreatePresentationUniqueKey() + { + if (!string.IsNullOrEmpty(PrimaryVersionId)) + { + return PrimaryVersionId; + } + + return base.CreatePresentationUniqueKey(); + } + + public override bool CanDownload() + { + if (VideoType == VideoType.Dvd || VideoType == VideoType.BluRay) + { + return false; + } + + return IsFileProtocol; + } + + protected override bool IsActiveRecording() + { + return LiveTvManager.GetActiveRecordingInfo(Path) != null; + } + + public override bool CanDelete() + { + if (IsActiveRecording()) + { + return false; + } + + return base.CanDelete(); + } + + public IEnumerable<Guid> GetAdditionalPartIds() + { + return AdditionalParts.Select(i => LibraryManager.GetNewItemId(i, typeof(Video))); + } + + public IEnumerable<Guid> GetLocalAlternateVersionIds() + { + return LocalAlternateVersions.Select(i => LibraryManager.GetNewItemId(i, typeof(Video))); + } + private string GetUserDataKey(string providerId) { var key = providerId + "-" + ExtraType.ToString().ToLowerInvariant(); @@ -338,47 +383,6 @@ namespace MediaBrowser.Controller.Entities .OrderBy(i => i.SortName); } - [IgnoreDataMember] - public override string ContainingFolderPath - { - get - { - if (IsStacked) - { - return System.IO.Path.GetDirectoryName(Path); - } - - if (!IsPlaceHolder) - { - if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd) - { - return Path; - } - } - - return base.ContainingFolderPath; - } - } - - [IgnoreDataMember] - public override string FileNameWithoutExtension - { - get - { - if (IsFileProtocol) - { - if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd) - { - return System.IO.Path.GetFileName(Path); - } - - return System.IO.Path.GetFileNameWithoutExtension(Path); - } - - return null; - } - } - internal override ItemUpdateType UpdateFromResolvedItem(BaseItem newItem) { var updateType = base.UpdateFromResolvedItem(newItem); @@ -390,11 +394,13 @@ namespace MediaBrowser.Controller.Entities AdditionalParts = newVideo.AdditionalParts; updateType |= ItemUpdateType.MetadataImport; } + if (!LocalAlternateVersions.SequenceEqual(newVideo.LocalAlternateVersions, StringComparer.Ordinal)) { LocalAlternateVersions = newVideo.LocalAlternateVersions; updateType |= ItemUpdateType.MetadataImport; } + if (VideoType != newVideo.VideoType) { VideoType = newVideo.VideoType; @@ -405,43 +411,6 @@ namespace MediaBrowser.Controller.Entities return updateType; } - public static string[] QueryPlayableStreamFiles(string rootPath, VideoType videoType) - { - if (videoType == VideoType.Dvd) - { - return FileSystem.GetFiles(rootPath, new[] { ".vob" }, false, true) - .OrderByDescending(i => i.Length) - .ThenBy(i => i.FullName) - .Take(1) - .Select(i => i.FullName) - .ToArray(); - } - if (videoType == VideoType.BluRay) - { - return FileSystem.GetFiles(rootPath, new[] { ".m2ts" }, false, true) - .OrderByDescending(i => i.Length) - .ThenBy(i => i.FullName) - .Take(1) - .Select(i => i.FullName) - .ToArray(); - } - return Array.Empty<string>(); - } - - /// <summary> - /// Gets a value indicating whether [is3 D]. - /// </summary> - /// <value><c>true</c> if [is3 D]; otherwise, <c>false</c>.</value> - [IgnoreDataMember] - public bool Is3D => Video3DFormat.HasValue; - - /// <summary> - /// Gets the type of the media. - /// </summary> - /// <value>The type of the media.</value> - [IgnoreDataMember] - public override string MediaType => Model.Entities.MediaType.Video; - protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) { var hasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); @@ -485,9 +454,10 @@ namespace MediaBrowser.Controller.Entities } } - public override void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken) + /// <inheritdoc /> + public override async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken) { - base.UpdateToRepository(updateReason, cancellationToken); + await base.UpdateToRepositoryAsync(updateReason, cancellationToken).ConfigureAwait(false); var localAlternates = GetLocalAlternateVersionIds() .Select(i => LibraryManager.GetItemById(i)) @@ -504,7 +474,7 @@ namespace MediaBrowser.Controller.Entities item.Genres = Genres; item.ProviderIds = ProviderIds; - item.UpdateToRepository(ItemUpdateType.MetadataDownload, cancellationToken); + await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataDownload, cancellationToken).ConfigureAwait(false); } } @@ -512,7 +482,8 @@ namespace MediaBrowser.Controller.Entities { if (!IsInMixedFolder) { - return new[] { + return new[] + { new FileSystemMetadata { FullName = ContainingFolderPath, @@ -535,7 +506,6 @@ namespace MediaBrowser.Controller.Entities { ItemId = Id, Index = DefaultVideoStreamIndex.Value - }).FirstOrDefault(); } diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs index 13e82fada..afdaf448b 100644 --- a/MediaBrowser.Controller/Entities/Year.cs +++ b/MediaBrowser.Controller/Entities/Year.cs @@ -1,16 +1,39 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; -using MediaBrowser.Model.Serialization; +using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities { /// <summary> - /// Class Year + /// Class Year. /// </summary> public class Year : BaseItem, IItemByName { + [JsonIgnore] + public override bool SupportsAncestors => false; + + [JsonIgnore] + public override bool SupportsPeople => false; + + /// <summary> + /// Gets the folder containing the item. + /// If the item is a folder, it returns the folder itself. + /// </summary> + /// <value>The containing folder path.</value> + [JsonIgnore] + public override string ContainingFolderPath => Path; + + public override bool CanDelete() + { + return false; + } + public override List<string> GetUserDataKeys() { var list = base.GetUserDataKeys(); @@ -19,14 +42,6 @@ namespace MediaBrowser.Controller.Entities return list; } - /// <summary> - /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself - /// </summary> - /// <value>The containing folder path.</value> - [IgnoreDataMember] - public override string ContainingFolderPath => Path; - public override double GetDefaultPrimaryImageAspectRatio() { double value = 2; @@ -35,14 +50,6 @@ namespace MediaBrowser.Controller.Entities return value; } - [IgnoreDataMember] - public override bool SupportsAncestors => false; - - public override bool CanDelete() - { - return false; - } - public override bool IsSaveLocalMetadataEnabled() { return true; @@ -50,9 +57,7 @@ namespace MediaBrowser.Controller.Entities public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) { - var usCulture = new CultureInfo("en-US"); - - if (!int.TryParse(Name, NumberStyles.Integer, usCulture, out var year)) + if (!int.TryParse(Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year)) { return new List<BaseItem>(); } @@ -72,9 +77,6 @@ namespace MediaBrowser.Controller.Entities return null; } - [IgnoreDataMember] - public override bool SupportsPeople => false; - public static string GetPath(string name) { return GetPath(name, true); @@ -103,15 +105,18 @@ namespace MediaBrowser.Controller.Entities Logger.LogDebug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); return true; } + return base.RequiresRefresh(); } /// <summary> - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true if changes were made. /// </summary> - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + /// <param name="replaceAllMetadata">Whether to replace all metadata.</param> + /// <returns>true if the item has change, else false.</returns> + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); var newPath = GetRebasedPath(); if (!string.Equals(Path, newPath, StringComparison.Ordinal)) diff --git a/MediaBrowser.Controller/Events/IEventConsumer.cs b/MediaBrowser.Controller/Events/IEventConsumer.cs new file mode 100644 index 000000000..93005134a --- /dev/null +++ b/MediaBrowser.Controller/Events/IEventConsumer.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Events +{ + /// <summary> + /// An interface representing a type that consumes events of type <c>T</c>. + /// </summary> + /// <typeparam name="T">The type of events this consumes.</typeparam> + public interface IEventConsumer<in T> + where T : EventArgs + { + /// <summary> + /// A method that is called when an event of type <c>T</c> is fired. + /// </summary> + /// <param name="eventArgs">The event.</param> + /// <returns>A task representing the consumption of the event.</returns> + Task OnEvent(T eventArgs); + } +} diff --git a/MediaBrowser.Controller/Events/IEventManager.cs b/MediaBrowser.Controller/Events/IEventManager.cs new file mode 100644 index 000000000..074e3f1fe --- /dev/null +++ b/MediaBrowser.Controller/Events/IEventManager.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Events +{ + /// <summary> + /// An interface that handles eventing. + /// </summary> + public interface IEventManager + { + /// <summary> + /// Publishes an event. + /// </summary> + /// <param name="eventArgs">the event arguments.</param> + /// <typeparam name="T">The type of event.</typeparam> + void Publish<T>(T eventArgs) + where T : EventArgs; + + /// <summary> + /// Publishes an event asynchronously. + /// </summary> + /// <param name="eventArgs">The event arguments.</param> + /// <typeparam name="T">The type of event.</typeparam> + /// <returns>A task representing the publishing of the event.</returns> + Task PublishAsync<T>(T eventArgs) + where T : EventArgs; + } +} diff --git a/MediaBrowser.Controller/Events/Session/SessionEndedEventArgs.cs b/MediaBrowser.Controller/Events/Session/SessionEndedEventArgs.cs new file mode 100644 index 000000000..3a331ad00 --- /dev/null +++ b/MediaBrowser.Controller/Events/Session/SessionEndedEventArgs.cs @@ -0,0 +1,19 @@ +using Jellyfin.Data.Events; +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.Events.Session +{ + /// <summary> + /// An event that fires when a session is ended. + /// </summary> + public class SessionEndedEventArgs : GenericEventArgs<SessionInfo> + { + /// <summary> + /// Initializes a new instance of the <see cref="SessionEndedEventArgs"/> class. + /// </summary> + /// <param name="arg">The session info.</param> + public SessionEndedEventArgs(SessionInfo arg) : base(arg) + { + } + } +} diff --git a/MediaBrowser.Controller/Events/Session/SessionStartedEventArgs.cs b/MediaBrowser.Controller/Events/Session/SessionStartedEventArgs.cs new file mode 100644 index 000000000..deeaaf55d --- /dev/null +++ b/MediaBrowser.Controller/Events/Session/SessionStartedEventArgs.cs @@ -0,0 +1,19 @@ +using Jellyfin.Data.Events; +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.Events.Session +{ + /// <summary> + /// An event that fires when a session is started. + /// </summary> + public class SessionStartedEventArgs : GenericEventArgs<SessionInfo> + { + /// <summary> + /// Initializes a new instance of the <see cref="SessionStartedEventArgs"/> class. + /// </summary> + /// <param name="arg">The session info.</param> + public SessionStartedEventArgs(SessionInfo arg) : base(arg) + { + } + } +} diff --git a/MediaBrowser.Controller/Events/Updates/PluginInstallationCancelledEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginInstallationCancelledEventArgs.cs new file mode 100644 index 000000000..0dd8b0dbf --- /dev/null +++ b/MediaBrowser.Controller/Events/Updates/PluginInstallationCancelledEventArgs.cs @@ -0,0 +1,19 @@ +using Jellyfin.Data.Events; +using MediaBrowser.Model.Updates; + +namespace MediaBrowser.Controller.Events.Updates +{ + /// <summary> + /// An event that occurs when a plugin installation is cancelled. + /// </summary> + public class PluginInstallationCancelledEventArgs : GenericEventArgs<InstallationInfo> + { + /// <summary> + /// Initializes a new instance of the <see cref="PluginInstallationCancelledEventArgs"/> class. + /// </summary> + /// <param name="arg">The installation info.</param> + public PluginInstallationCancelledEventArgs(InstallationInfo arg) : base(arg) + { + } + } +} diff --git a/MediaBrowser.Controller/Events/Updates/PluginInstalledEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginInstalledEventArgs.cs new file mode 100644 index 000000000..c1d503a7e --- /dev/null +++ b/MediaBrowser.Controller/Events/Updates/PluginInstalledEventArgs.cs @@ -0,0 +1,19 @@ +using Jellyfin.Data.Events; +using MediaBrowser.Model.Updates; + +namespace MediaBrowser.Controller.Events.Updates +{ + /// <summary> + /// An event that occurs when a plugin is installed. + /// </summary> + public class PluginInstalledEventArgs : GenericEventArgs<InstallationInfo> + { + /// <summary> + /// Initializes a new instance of the <see cref="PluginInstalledEventArgs"/> class. + /// </summary> + /// <param name="arg">The installation info.</param> + public PluginInstalledEventArgs(InstallationInfo arg) : base(arg) + { + } + } +} diff --git a/MediaBrowser.Controller/Events/Updates/PluginInstallingEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginInstallingEventArgs.cs new file mode 100644 index 000000000..7a9866834 --- /dev/null +++ b/MediaBrowser.Controller/Events/Updates/PluginInstallingEventArgs.cs @@ -0,0 +1,19 @@ +using Jellyfin.Data.Events; +using MediaBrowser.Model.Updates; + +namespace MediaBrowser.Controller.Events.Updates +{ + /// <summary> + /// An event that occurs when a plugin is installing. + /// </summary> + public class PluginInstallingEventArgs : GenericEventArgs<InstallationInfo> + { + /// <summary> + /// Initializes a new instance of the <see cref="PluginInstallingEventArgs"/> class. + /// </summary> + /// <param name="arg">The installation info.</param> + public PluginInstallingEventArgs(InstallationInfo arg) : base(arg) + { + } + } +} diff --git a/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs new file mode 100644 index 000000000..0f27be9bb --- /dev/null +++ b/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs @@ -0,0 +1,19 @@ +using Jellyfin.Data.Events; +using MediaBrowser.Model.Plugins; + +namespace MediaBrowser.Controller.Events.Updates +{ + /// <summary> + /// An event that occurs when a plugin is uninstalled. + /// </summary> + public class PluginUninstalledEventArgs : GenericEventArgs<PluginInfo> + { + /// <summary> + /// Initializes a new instance of the <see cref="PluginUninstalledEventArgs"/> class. + /// </summary> + /// <param name="arg">The plugin.</param> + public PluginUninstalledEventArgs(PluginInfo arg) : base(arg) + { + } + } +} diff --git a/MediaBrowser.Controller/Events/Updates/PluginUpdatedEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginUpdatedEventArgs.cs new file mode 100644 index 000000000..b078e06dc --- /dev/null +++ b/MediaBrowser.Controller/Events/Updates/PluginUpdatedEventArgs.cs @@ -0,0 +1,19 @@ +using Jellyfin.Data.Events; +using MediaBrowser.Model.Updates; + +namespace MediaBrowser.Controller.Events.Updates +{ + /// <summary> + /// An event that occurs when a plugin is updated. + /// </summary> + public class PluginUpdatedEventArgs : GenericEventArgs<InstallationInfo> + { + /// <summary> + /// Initializes a new instance of the <see cref="PluginUpdatedEventArgs"/> class. + /// </summary> + /// <param name="arg">The installation info.</param> + public PluginUpdatedEventArgs(InstallationInfo arg) : base(arg) + { + } + } +} diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs new file mode 100644 index 000000000..f9285c768 --- /dev/null +++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs @@ -0,0 +1,101 @@ +using System; +using Microsoft.Extensions.Configuration; + +namespace MediaBrowser.Controller.Extensions +{ + /// <summary> + /// Configuration extensions for <c>MediaBrowser.Controller</c>. + /// </summary> + public static class ConfigurationExtensions + { + /// <summary> + /// The key for a setting that specifies the default redirect path + /// to use for requests where the URL base prefix is invalid or missing.. + /// </summary> + public const string DefaultRedirectKey = "DefaultRedirectPath"; + + /// <summary> + /// The key for a setting that indicates whether the application should host web client content. + /// </summary> + public const string HostWebClientKey = "hostwebclient"; + + /// <summary> + /// The key for the FFmpeg probe size option. + /// </summary> + public const string FfmpegProbeSizeKey = "FFmpeg:probesize"; + + /// <summary> + /// The key for the FFmpeg analyze duration option. + /// </summary> + public const string FfmpegAnalyzeDurationKey = "FFmpeg:analyzeduration"; + + /// <summary> + /// The key for the FFmpeg path option. + /// </summary> + public const string FfmpegPathKey = "ffmpeg"; + + /// <summary> + /// The key for a setting that indicates whether playlists should allow duplicate entries. + /// </summary> + public const string PlaylistsAllowDuplicatesKey = "playlists:allowDuplicates"; + + /// <summary> + /// The key for a setting that indicates whether kestrel should bind to a unix socket. + /// </summary> + public const string BindToUnixSocketKey = "kestrel:socket"; + + /// <summary> + /// The key for the unix socket path. + /// </summary> + public const string UnixSocketPathKey = "kestrel:socketPath"; + + /// <summary> + /// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>. + /// </summary> + /// <param name="configuration">The configuration to retrieve the value from.</param> + /// <returns>The parsed config value.</returns> + /// <exception cref="FormatException">The config value is not a valid bool string. See <see cref="bool.Parse(string)"/>.</exception> + public static bool HostWebClient(this IConfiguration configuration) + => configuration.GetValue<bool>(HostWebClientKey); + + /// <summary> + /// Gets the FFmpeg probe size from the <see cref="IConfiguration" />. + /// </summary> + /// <param name="configuration">The configuration to read the setting from.</param> + /// <returns>The FFmpeg probe size option.</returns> + public static string GetFFmpegProbeSize(this IConfiguration configuration) + => configuration[FfmpegProbeSizeKey]; + + /// <summary> + /// Gets the FFmpeg analyze duration from the <see cref="IConfiguration" />. + /// </summary> + /// <param name="configuration">The configuration to read the setting from.</param> + /// <returns>The FFmpeg analyze duration option.</returns> + public static string GetFFmpegAnalyzeDuration(this IConfiguration configuration) + => configuration[FfmpegAnalyzeDurationKey]; + + /// <summary> + /// Gets a value indicating whether playlists should allow duplicate entries from the <see cref="IConfiguration"/>. + /// </summary> + /// <param name="configuration">The configuration to read the setting from.</param> + /// <returns>True if playlists should allow duplicates, otherwise false.</returns> + public static bool DoPlaylistsAllowDuplicates(this IConfiguration configuration) + => configuration.GetValue<bool>(PlaylistsAllowDuplicatesKey); + + /// <summary> + /// Gets a value indicating whether kestrel should bind to a unix socket from the <see cref="IConfiguration" />. + /// </summary> + /// <param name="configuration">The configuration to read the setting from.</param> + /// <returns><c>true</c> if kestrel should bind to a unix socket, otherwise <c>false</c>.</returns> + public static bool UseUnixSocket(this IConfiguration configuration) + => configuration.GetValue<bool>(BindToUnixSocketKey); + + /// <summary> + /// Gets the path for the unix socket from the <see cref="IConfiguration" />. + /// </summary> + /// <param name="configuration">The configuration to read the setting from.</param> + /// <returns>The unix socket path.</returns> + public static string GetUnixSocketPath(this IConfiguration configuration) + => configuration[UnixSocketPathKey]; + } +} diff --git a/MediaBrowser.Controller/Extensions/StringExtensions.cs b/MediaBrowser.Controller/Extensions/StringExtensions.cs deleted file mode 100644 index b1aaf6534..000000000 --- a/MediaBrowser.Controller/Extensions/StringExtensions.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; - -namespace MediaBrowser.Controller.Extensions -{ - /// <summary> - /// Class BaseExtensions - /// </summary> - public static class StringExtensions - { - public static string RemoveDiacritics(this string text) - { - if (text == null) - { - throw new ArgumentNullException(nameof(text)); - } - - var chars = Normalize(text, NormalizationForm.FormD) - .Where(ch => CharUnicodeInfo.GetUnicodeCategory(ch) != UnicodeCategory.NonSpacingMark); - - return Normalize(string.Concat(chars), NormalizationForm.FormC); - } - - private static string Normalize(string text, NormalizationForm form, bool stripStringOnFailure = true) - { - if (stripStringOnFailure) - { - try - { - return text.Normalize(form); - } - catch (ArgumentException) - { - // will throw if input contains invalid unicode chars - // https://mnaoumov.wordpress.com/2014/06/14/stripping-invalid-characters-from-utf-16-strings/ - text = Regex.Replace(text, "([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])", ""); - return Normalize(text, form, false); - } - } - - try - { - return text.Normalize(form); - } - catch (ArgumentException) - { - // if it still fails, return the original text - return text; - } - } - } -} diff --git a/MediaBrowser.Controller/IDisplayPreferencesManager.cs b/MediaBrowser.Controller/IDisplayPreferencesManager.cs new file mode 100644 index 000000000..1678d5067 --- /dev/null +++ b/MediaBrowser.Controller/IDisplayPreferencesManager.cs @@ -0,0 +1,69 @@ +#nullable disable + +using System; +using System.Collections.Generic; +using Jellyfin.Data.Entities; + +namespace MediaBrowser.Controller +{ + /// <summary> + /// Manages the storage and retrieval of display preferences. + /// </summary> + public interface IDisplayPreferencesManager + { + /// <summary> + /// Gets the display preferences for the user and client. + /// </summary> + /// <remarks> + /// This will create the display preferences if it does not exist, but it will not save automatically. + /// </remarks> + /// <param name="userId">The user's id.</param> + /// <param name="itemId">The item id.</param> + /// <param name="client">The client string.</param> + /// <returns>The associated display preferences.</returns> + DisplayPreferences GetDisplayPreferences(Guid userId, Guid itemId, string client); + + /// <summary> + /// Gets the default item display preferences for the user and client. + /// </summary> + /// <remarks> + /// This will create the item display preferences if it does not exist, but it will not save automatically. + /// </remarks> + /// <param name="userId">The user id.</param> + /// <param name="itemId">The item id.</param> + /// <param name="client">The client string.</param> + /// <returns>The item display preferences.</returns> + ItemDisplayPreferences GetItemDisplayPreferences(Guid userId, Guid itemId, string client); + + /// <summary> + /// Gets all of the item display preferences for the user and client. + /// </summary> + /// <param name="userId">The user id.</param> + /// <param name="client">The client string.</param> + /// <returns>A list of item display preferences.</returns> + IList<ItemDisplayPreferences> ListItemDisplayPreferences(Guid userId, string client); + + /// <summary> + /// Gets all of the custom item display preferences for the user and client. + /// </summary> + /// <param name="userId">The user id.</param> + /// <param name="itemId">The item id.</param> + /// <param name="client">The client string.</param> + /// <returns>The dictionary of custom item display preferences.</returns> + Dictionary<string, string> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client); + + /// <summary> + /// Sets the custom item display preference for the user and client. + /// </summary> + /// <param name="userId">The user id.</param> + /// <param name="itemId">The item id.</param> + /// <param name="client">The client id.</param> + /// <param name="customPreferences">A dictionary of custom item display preferences.</param> + void SetCustomItemDisplayPreferences(Guid userId, Guid itemId, string client, Dictionary<string, string> customPreferences); + + /// <summary> + /// Saves changes made to the database. + /// </summary> + void SaveChanges(); + } +} diff --git a/MediaBrowser.Controller/IO/FileData.cs b/MediaBrowser.Controller/IO/FileData.cs index 4bbb60283..b8a0bf331 100644 --- a/MediaBrowser.Controller/IO/FileData.cs +++ b/MediaBrowser.Controller/IO/FileData.cs @@ -8,34 +8,25 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.IO { /// <summary> - /// Provides low level File access that is much faster than the File/Directory api's + /// Provides low level File access that is much faster than the File/Directory api's. /// </summary> public static class FileData { - private static Dictionary<string, FileSystemMetadata> GetFileSystemDictionary(FileSystemMetadata[] list) - { - var dict = new Dictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase); - - foreach (var file in list) - { - dict[file.FullName] = file; - } - return dict; - } - /// <summary> /// Gets the filtered file system entries. /// </summary> /// <param name="directoryService">The directory service.</param> /// <param name="path">The path.</param> /// <param name="fileSystem">The file system.</param> + /// <param name="appHost">The application host.</param> /// <param name="logger">The logger.</param> /// <param name="args">The args.</param> /// <param name="flattenFolderDepth">The flatten folder depth.</param> /// <param name="resolveShortcuts">if set to <c>true</c> [resolve shortcuts].</param> /// <returns>Dictionary{System.StringFileSystemInfo}.</returns> - /// <exception cref="ArgumentNullException">path</exception> - public static FileSystemMetadata[] GetFilteredFileSystemEntries(IDirectoryService directoryService, + /// <exception cref="ArgumentNullException"><paramref name="path" /> is <c>null</c> or empty.</exception> + public static FileSystemMetadata[] GetFilteredFileSystemEntries( + IDirectoryService directoryService, string path, IFileSystem fileSystem, IServerApplicationHost appHost, @@ -48,6 +39,7 @@ namespace MediaBrowser.Controller.IO { throw new ArgumentNullException(nameof(path)); } + if (args == null) { throw new ArgumentNullException(nameof(args)); @@ -76,7 +68,7 @@ namespace MediaBrowser.Controller.IO if (string.IsNullOrEmpty(newPath)) { - //invalid shortcut - could be old or target could just be unavailable + // invalid shortcut - could be old or target could just be unavailable logger.LogWarning("Encountered invalid shortcut: " + fullName); continue; } @@ -115,9 +107,8 @@ namespace MediaBrowser.Controller.IO returnResult[index] = value; index++; } + return returnResult; } - } - } diff --git a/MediaBrowser.Controller/IResourceFileManager.cs b/MediaBrowser.Controller/IResourceFileManager.cs deleted file mode 100644 index f70ea6a17..000000000 --- a/MediaBrowser.Controller/IResourceFileManager.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; - -namespace MediaBrowser.Controller -{ - public interface IResourceFileManager - { - Task<object> GetStaticFileResult(IRequest request, string basePath, string virtualPath, string contentType, TimeSpan? cacheDuration); - - Stream GetResourceFileStream(string basePath, string virtualPath); - - string ReadAllText(string basePath, string virtualPath); - } -} diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 22797aa0d..7e5e742f5 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -1,27 +1,21 @@ -using System; +#nullable disable + +#pragma warning disable CS1591 + using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; +using System.Net; using MediaBrowser.Common; -using MediaBrowser.Model.Net; using MediaBrowser.Model.System; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller { /// <summary> - /// Interface IServerApplicationHost + /// Interface IServerApplicationHost. /// </summary> public interface IServerApplicationHost : IApplicationHost { - event EventHandler HasUpdateAvailableChanged; - - /// <summary> - /// Gets the system info. - /// </summary> - /// <returns>SystemInfo.</returns> - Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken); - - Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken); + bool CoreStartupHasCompleted { get; } bool CanLaunchWebBrowser { get; } @@ -38,16 +32,9 @@ namespace MediaBrowser.Controller int HttpsPort { get; } /// <summary> - /// Gets a value indicating whether [supports HTTPS]. - /// </summary> - /// <value><c>true</c> if [supports HTTPS]; otherwise, <c>false</c>.</value> - bool EnableHttps { get; } - - /// <summary> - /// Gets a value indicating whether this instance has update available. + /// Gets a value indicating whether the server should listen on an HTTPS port. /// </summary> - /// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value> - bool HasUpdateAvailable { get; } + bool ListenWithHttps { get; } /// <summary> /// Gets the name of the friendly. @@ -56,38 +43,70 @@ namespace MediaBrowser.Controller string FriendlyName { get; } /// <summary> - /// Gets the local ip address. + /// Gets the configured published server url. /// </summary> - /// <value>The local ip address.</value> - Task<List<IpAddressInfo>> GetLocalIpAddresses(CancellationToken cancellationToken); + string PublishedServerUrl { get; } /// <summary> - /// Gets the local API URL. + /// Gets the system info. /// </summary> - /// <value>The local API URL.</value> - Task<string> GetLocalApiUrl(CancellationToken cancellationToken); + /// <param name="request">The HTTP request.</param> + /// <returns>SystemInfo.</returns> + SystemInfo GetSystemInfo(HttpRequest request); + + PublicSystemInfo GetPublicSystemInfo(HttpRequest request); /// <summary> - /// Gets the local API URL. + /// Gets a URL specific for the request. /// </summary> - /// <param name="host">The host.</param> - /// <returns>System.String.</returns> - string GetLocalApiUrl(string host); + /// <param name="request">The <see cref="HttpRequest"/> instance.</param> + /// <param name="port">Optional port number.</param> + /// <returns>An accessible URL.</returns> + string GetSmartApiUrl(HttpRequest request, int? port = null); /// <summary> - /// Gets the local API URL. + /// Gets a URL specific for the request. /// </summary> - string GetLocalApiUrl(IpAddressInfo address); + /// <param name="remoteAddr">The remote <see cref="IPAddress"/> of the connection.</param> + /// <param name="port">Optional port number.</param> + /// <returns>An accessible URL.</returns> + string GetSmartApiUrl(IPAddress remoteAddr, int? port = null); - void LaunchUrl(string url); - - void EnableLoopback(string appName); + /// <summary> + /// Gets a URL specific for the request. + /// </summary> + /// <param name="hostname">The hostname used in the connection.</param> + /// <param name="port">Optional port number.</param> + /// <returns>An accessible URL.</returns> + string GetSmartApiUrl(string hostname, int? port = null); - string PackageRuntime { get; } + /// <summary> + /// Gets a localhost URL that can be used to access the API using the loop-back IP address. + /// over HTTP (not HTTPS). + /// </summary> + /// <returns>The API URL.</returns> + string GetLoopbackHttpApiUrl(); - WakeOnLanInfo[] GetWakeOnLanInfo(); + /// <summary> + /// Gets a local (LAN) URL that can be used to access the API. + /// Note: if passing non-null scheme or port it is up to the caller to ensure they form the correct pair. + /// </summary> + /// <param name="hostname">The hostname to use in the URL.</param> + /// <param name="scheme"> + /// The scheme to use for the URL. If null, the scheme will be selected automatically, + /// preferring HTTPS, if available. + /// </param> + /// <param name="port"> + /// The port to use for the URL. If null, the port will be selected automatically, + /// preferring the HTTPS port, if available. + /// </param> + /// <returns>The API URL.</returns> + string GetLocalApiUrl(string hostname, string scheme = null, int? port = null); + + IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo(); string ExpandVirtualPath(string path); + string ReverseVirtualPath(string path); } } diff --git a/MediaBrowser.Controller/IServerApplicationPaths.cs b/MediaBrowser.Controller/IServerApplicationPaths.cs index 15d7e9f62..1890dbb36 100644 --- a/MediaBrowser.Controller/IServerApplicationPaths.cs +++ b/MediaBrowser.Controller/IServerApplicationPaths.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using MediaBrowser.Common.Configuration; namespace MediaBrowser.Controller @@ -5,37 +9,25 @@ namespace MediaBrowser.Controller public interface IServerApplicationPaths : IApplicationPaths { /// <summary> - /// Gets the path to the base root media directory + /// Gets the path to the base root media directory. /// </summary> /// <value>The root folder path.</value> string RootFolderPath { get; } /// <summary> - /// Gets the application resources path. This is the path to the folder containing resources that are deployed as part of the application - /// </summary> - /// <value>The application resources path.</value> - string ApplicationResourcesPath { get; } - - /// <summary> /// Gets the path to the default user view directory. Used if no specific user view is defined. /// </summary> /// <value>The default user views path.</value> string DefaultUserViewsPath { get; } /// <summary> - /// Gets the path to localization data. - /// </summary> - /// <value>The localization path.</value> - string LocalizationPath { get; } - - /// <summary> - /// Gets the path to the People directory + /// Gets the path to the People directory. /// </summary> /// <value>The people path.</value> string PeoplePath { get; } /// <summary> - /// Gets the path to the Genre directory + /// Gets the path to the Genre directory. /// </summary> /// <value>The genre path.</value> string GenrePath { get; } @@ -47,25 +39,25 @@ namespace MediaBrowser.Controller string MusicGenrePath { get; } /// <summary> - /// Gets the path to the Studio directory + /// Gets the path to the Studio directory. /// </summary> /// <value>The studio path.</value> string StudioPath { get; } /// <summary> - /// Gets the path to the Year directory + /// Gets the path to the Year directory. /// </summary> /// <value>The year path.</value> string YearPath { get; } /// <summary> - /// Gets the path to the General IBN directory + /// Gets the path to the General IBN directory. /// </summary> /// <value>The general path.</value> string GeneralPath { get; } /// <summary> - /// Gets the path to the Ratings IBN directory + /// Gets the path to the Ratings IBN directory. /// </summary> /// <value>The ratings path.</value> string RatingsPath { get; } @@ -77,26 +69,32 @@ namespace MediaBrowser.Controller string MediaInfoImagesPath { get; } /// <summary> - /// Gets the path to the user configuration directory + /// Gets the path to the user configuration directory. /// </summary> /// <value>The user configuration directory path.</value> string UserConfigurationDirectoryPath { get; } /// <summary> - /// Gets the transcoding temporary path. + /// Gets the default internal metadata path. /// </summary> - /// <value>The transcoding temporary path.</value> - string TranscodingTempPath { get; } + string DefaultInternalMetadataPath { get; } /// <summary> - /// Gets the internal metadata path. + /// Gets the internal metadata path, either a custom path or the default. /// </summary> /// <value>The internal metadata path.</value> string InternalMetadataPath { get; } + + /// <summary> + /// Gets the virtual internal metadata path, either a custom path or the default. + /// </summary> + /// <value>The virtual internal metadata path.</value> string VirtualInternalMetadataPath { get; } + /// <summary> + /// Gets the path to the artists directory. + /// </summary> + /// <value>The artists path.</value> string ArtistsPath { get; } - - string GetTranscodingTempPath(); } } diff --git a/MediaBrowser.Controller/Library/DeleteOptions.cs b/MediaBrowser.Controller/Library/DeleteOptions.cs index 751b90481..408e70284 100644 --- a/MediaBrowser.Controller/Library/DeleteOptions.cs +++ b/MediaBrowser.Controller/Library/DeleteOptions.cs @@ -1,13 +1,16 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Library { public class DeleteOptions { - public bool DeleteFileLocation { get; set; } - public bool DeleteFromExternalProvider { get; set; } - public DeleteOptions() { DeleteFromExternalProvider = true; } + + public bool DeleteFileLocation { get; set; } + + public bool DeleteFromExternalProvider { get; set; } } } diff --git a/MediaBrowser.Controller/Library/IDirectStreamProvider.cs b/MediaBrowser.Controller/Library/IDirectStreamProvider.cs new file mode 100644 index 000000000..96f8b7eba --- /dev/null +++ b/MediaBrowser.Controller/Library/IDirectStreamProvider.cs @@ -0,0 +1,19 @@ +using System.IO; + +namespace MediaBrowser.Controller.Library +{ + /// <summary> + /// The direct live TV stream provider. + /// </summary> + /// <remarks> + /// Deprecated. + /// </remarks> + public interface IDirectStreamProvider + { + /// <summary> + /// Gets the live stream, shared streams seek to the end of the file first. + /// </summary> + /// <returns>The stream.</returns> + Stream GetStream(); + } +} diff --git a/MediaBrowser.Controller/Library/IIntroProvider.cs b/MediaBrowser.Controller/Library/IIntroProvider.cs index d9d1ca8c7..a74d1b9f0 100644 --- a/MediaBrowser.Controller/Library/IIntroProvider.cs +++ b/MediaBrowser.Controller/Library/IIntroProvider.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Collections.Generic; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; @@ -5,28 +7,28 @@ using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Library { /// <summary> - /// Class BaseIntroProvider + /// Class BaseIntroProvider. /// </summary> public interface IIntroProvider { /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + string Name { get; } + + /// <summary> /// Gets the intros. /// </summary> /// <param name="item">The item.</param> /// <param name="user">The user.</param> /// <returns>IEnumerable{System.String}.</returns> - Task<IEnumerable<IntroInfo>> GetIntros(BaseItem item, User user); + Task<IEnumerable<IntroInfo>> GetIntros(BaseItem item, Jellyfin.Data.Entities.User user); /// <summary> /// Gets all intro files. /// </summary> /// <returns>IEnumerable{System.String}.</returns> IEnumerable<string> GetAllIntroFiles(); - - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - string Name { get; } } } diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 511356aa4..d40e56c7d 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -1,11 +1,17 @@ +#nullable disable + +#pragma warning disable CA1002, CS1591 + using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Emby.Naming.Common; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Sorting; @@ -14,42 +20,70 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Querying; +using Episode = MediaBrowser.Controller.Entities.TV.Episode; +using Genre = MediaBrowser.Controller.Entities.Genre; +using Person = MediaBrowser.Controller.Entities.Person; namespace MediaBrowser.Controller.Library { /// <summary> - /// Interface ILibraryManager + /// Interface ILibraryManager. /// </summary> public interface ILibraryManager { /// <summary> + /// Occurs when [item added]. + /// </summary> + event EventHandler<ItemChangeEventArgs> ItemAdded; + + /// <summary> + /// Occurs when [item updated]. + /// </summary> + event EventHandler<ItemChangeEventArgs> ItemUpdated; + + /// <summary> + /// Occurs when [item removed]. + /// </summary> + event EventHandler<ItemChangeEventArgs> ItemRemoved; + + /// <summary> + /// Gets the root folder. + /// </summary> + /// <value>The root folder.</value> + AggregateFolder RootFolder { get; } + + bool IsScanRunning { get; } + + /// <summary> /// Resolves the path. /// </summary> /// <param name="fileInfo">The file information.</param> /// <param name="parent">The parent.</param> /// <returns>BaseItem.</returns> - BaseItem ResolvePath(FileSystemMetadata fileInfo, + BaseItem ResolvePath( + FileSystemMetadata fileInfo, Folder parent = null); /// <summary> - /// Resolves a set of files into a list of BaseItem + /// Resolves a set of files into a list of BaseItem. /// </summary> - IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, + /// <param name="files">The list of tiles.</param> + /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param> + /// <param name="parent">The parent folder.</param> + /// <param name="libraryOptions">The library options.</param> + /// <param name="collectionType">The collection type.</param> + /// <returns>The items resolved from the paths.</returns> + IEnumerable<BaseItem> ResolvePaths( + IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, string collectionType = null); /// <summary> - /// Gets the root folder. + /// Gets a Person. /// </summary> - /// <value>The root folder.</value> - AggregateFolder RootFolder { get; } - - /// <summary> - /// Gets a Person - /// </summary> - /// <param name="name">The name.</param> + /// <param name="name">The name of the person.</param> /// <returns>Task{Person}.</returns> Person GetPerson(string name); @@ -57,43 +91,46 @@ namespace MediaBrowser.Controller.Library /// Finds the by path. /// </summary> /// <param name="path">The path.</param> + /// <param name="isFolder"><c>true</c> is the path is a directory; otherwise <c>false</c>.</param> /// <returns>BaseItem.</returns> BaseItem FindByPath(string path, bool? isFolder); /// <summary> /// Gets the artist. /// </summary> - /// <param name="name">The name.</param> + /// <param name="name">The name of the artist.</param> /// <returns>Task{Artist}.</returns> MusicArtist GetArtist(string name); + MusicArtist GetArtist(string name, DtoOptions options); + /// <summary> - /// Gets a Studio + /// Gets a Studio. /// </summary> - /// <param name="name">The name.</param> + /// <param name="name">The name of the studio.</param> /// <returns>Task{Studio}.</returns> Studio GetStudio(string name); /// <summary> - /// Gets a Genre + /// Gets a Genre. /// </summary> - /// <param name="name">The name.</param> + /// <param name="name">The name of the genre.</param> /// <returns>Task{Genre}.</returns> Genre GetGenre(string name); /// <summary> /// Gets the genre. /// </summary> - /// <param name="name">The name.</param> + /// <param name="name">The name of the music genre.</param> /// <returns>Task{MusicGenre}.</returns> MusicGenre GetMusicGenre(string name); /// <summary> - /// Gets a Year + /// Gets a Year. /// </summary> /// <param name="value">The value.</param> /// <returns>Task{Year}.</returns> - /// <exception cref="ArgumentOutOfRangeException"></exception> + /// <exception cref="ArgumentOutOfRangeException">Throws if year is invalid.</exception> Year GetYear(int value); /// <summary> @@ -106,7 +143,7 @@ namespace MediaBrowser.Controller.Library Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress); /// <summary> - /// Reloads the root media folder + /// Reloads the root media folder. /// </summary> /// <param name="progress">The progress.</param> /// <param name="cancellationToken">The cancellation token.</param> @@ -118,7 +155,7 @@ namespace MediaBrowser.Controller.Library /// </summary> void QueueLibraryScan(); - void UpdateImages(BaseItem item); + Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false); /// <summary> /// Gets the default view. @@ -157,7 +194,8 @@ namespace MediaBrowser.Controller.Library /// <param name="introProviders">The intro providers.</param> /// <param name="itemComparers">The item comparers.</param> /// <param name="postscanTasks">The postscan tasks.</param> - void AddParts(IEnumerable<IResolverIgnoreRule> rules, + void AddParts( + IEnumerable<IResolverIgnoreRule> rules, IEnumerable<IItemResolver> resolvers, IEnumerable<IIntroProvider> introProviders, IEnumerable<IBaseItemComparer> itemComparers, @@ -172,6 +210,7 @@ namespace MediaBrowser.Controller.Library /// <param name="sortOrder">The sort order.</param> /// <returns>IEnumerable{BaseItem}.</returns> IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<string> sortBy, SortOrder sortOrder); + IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ValueTuple<string, SortOrder>> orderBy); /// <summary> @@ -183,18 +222,37 @@ namespace MediaBrowser.Controller.Library /// <summary> /// Creates the item. /// </summary> + /// <param name="item">Item to create.</param> + /// <param name="parent">Parent of new item.</param> void CreateItem(BaseItem item, BaseItem parent); /// <summary> /// Creates the items. /// </summary> - void CreateItems(IEnumerable<BaseItem> items, BaseItem parent, CancellationToken cancellationToken); + /// <param name="items">Items to create.</param> + /// <param name="parent">Parent of new items.</param> + /// <param name="cancellationToken">CancellationToken to use for operation.</param> + void CreateItems(IReadOnlyList<BaseItem> items, BaseItem parent, CancellationToken cancellationToken); /// <summary> /// Updates the item. /// </summary> - void UpdateItems(IEnumerable<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken); - void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken); + /// <param name="items">Items to update.</param> + /// <param name="parent">Parent of updated items.</param> + /// <param name="updateReason">Reason for update.</param> + /// <param name="cancellationToken">CancellationToken to use for operation.</param> + /// <returns>Returns a Task that can be awaited.</returns> + Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken); + + /// <summary> + /// Updates the item. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="parent">The parent item.</param> + /// <param name="updateReason">The update reason.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Returns a Task that can be awaited.</returns> + Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken); /// <summary> /// Retrieves the item. @@ -203,22 +261,6 @@ namespace MediaBrowser.Controller.Library /// <returns>BaseItem.</returns> BaseItem RetrieveItem(Guid id); - bool IsScanRunning { get; } - - /// <summary> - /// Occurs when [item added]. - /// </summary> - event EventHandler<ItemChangeEventArgs> ItemAdded; - - /// <summary> - /// Occurs when [item updated]. - /// </summary> - event EventHandler<ItemChangeEventArgs> ItemUpdated; - /// <summary> - /// Occurs when [item removed]. - /// </summary> - event EventHandler<ItemChangeEventArgs> ItemRemoved; - /// <summary> /// Finds the type of the collection. /// </summary> @@ -263,16 +305,25 @@ namespace MediaBrowser.Controller.Library /// <summary> /// Deletes the item. /// </summary> + /// <param name="item">Item to delete.</param> + /// <param name="options">Options to use for deletion.</param> void DeleteItem(BaseItem item, DeleteOptions options); /// <summary> /// Deletes the item. /// </summary> + /// <param name="item">Item to delete.</param> + /// <param name="options">Options to use for deletion.</param> + /// <param name="notifyParentItem">Notify parent of deletion.</param> void DeleteItem(BaseItem item, DeleteOptions options, bool notifyParentItem); /// <summary> /// Deletes the item. /// </summary> + /// <param name="item">Item to delete.</param> + /// <param name="options">Options to use for deletion.</param> + /// <param name="parent">Parent of item.</param> + /// <param name="notifyParentItem">Notify parent of deletion.</param> void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem); /// <summary> @@ -283,7 +334,9 @@ namespace MediaBrowser.Controller.Library /// <param name="parentId">The parent identifier.</param> /// <param name="viewType">Type of the view.</param> /// <param name="sortName">Name of the sort.</param> - UserView GetNamedView(User user, + /// <returns>The named view.</returns> + UserView GetNamedView( + User user, string name, Guid parentId, string viewType, @@ -296,7 +349,9 @@ namespace MediaBrowser.Controller.Library /// <param name="name">The name.</param> /// <param name="viewType">Type of the view.</param> /// <param name="sortName">Name of the sort.</param> - UserView GetNamedView(User user, + /// <returns>The named view.</returns> + UserView GetNamedView( + User user, string name, string viewType, string sortName); @@ -307,7 +362,9 @@ namespace MediaBrowser.Controller.Library /// <param name="name">The name.</param> /// <param name="viewType">Type of the view.</param> /// <param name="sortName">Name of the sort.</param> - UserView GetNamedView(string name, + /// <returns>The named view.</returns> + UserView GetNamedView( + string name, string viewType, string sortName); @@ -319,7 +376,9 @@ namespace MediaBrowser.Controller.Library /// <param name="viewType">Type of the view.</param> /// <param name="sortName">Name of the sort.</param> /// <param name="uniqueId">The unique identifier.</param> - UserView GetNamedView(string name, + /// <returns>The named view.</returns> + UserView GetNamedView( + string name, Guid parentId, string viewType, string sortName, @@ -331,9 +390,11 @@ namespace MediaBrowser.Controller.Library /// <param name="parent">The parent.</param> /// <param name="viewType">Type of the view.</param> /// <param name="sortName">Name of the sort.</param> - UserView GetShadowView(BaseItem parent, - string viewType, - string sortName); + /// <returns>The shadow view.</returns> + UserView GetShadowView( + BaseItem parent, + string viewType, + string sortName); /// <summary> /// Determines whether [is video file] [the specified path]. @@ -349,9 +410,6 @@ namespace MediaBrowser.Controller.Library /// <returns><c>true</c> if [is audio file] [the specified path]; otherwise, <c>false</c>.</returns> bool IsAudioFile(string path); - bool IsAudioFile(string path, LibraryOptions libraryOptions); - bool IsVideoFile(string path, LibraryOptions libraryOptions); - /// <summary> /// Gets the season number from path. /// </summary> @@ -362,6 +420,9 @@ namespace MediaBrowser.Controller.Library /// <summary> /// Fills the missing episode numbers from path. /// </summary> + /// <param name="episode">Episode to use.</param> + /// <param name="forceRefresh">Option to force refresh of episode numbers.</param> + /// <returns>True if successful.</returns> bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh); /// <summary> @@ -386,7 +447,9 @@ namespace MediaBrowser.Controller.Library /// <param name="fileSystemChildren">The file system children.</param> /// <param name="directoryService">The directory service.</param> /// <returns>IEnumerable<Trailer>.</returns> - IEnumerable<Video> FindTrailers(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, + IEnumerable<Video> FindTrailers( + BaseItem owner, + List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService); /// <summary> @@ -396,7 +459,9 @@ namespace MediaBrowser.Controller.Library /// <param name="fileSystemChildren">The file system children.</param> /// <param name="directoryService">The directory service.</param> /// <returns>IEnumerable<Video>.</returns> - IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, + IEnumerable<Video> FindExtras( + BaseItem owner, + List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService); /// <summary> @@ -439,6 +504,15 @@ namespace MediaBrowser.Controller.Library void UpdatePeople(BaseItem item, List<PersonInfo> people); /// <summary> + /// Asynchronously updates the people. + /// </summary> + /// <param name="item">The item.</param> + /// <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); + + /// <summary> /// Gets the item ids. /// </summary> /// <param name="query">The query.</param> @@ -491,6 +565,9 @@ namespace MediaBrowser.Controller.Library /// <summary> /// Gets the items. /// </summary> + /// <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); /// <summary> @@ -514,23 +591,45 @@ namespace MediaBrowser.Controller.Library Guid GetMusicGenreId(string name); - Task AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary); + Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, bool refreshLibrary); + Task RemoveVirtualFolder(string name, bool refreshLibrary); - void AddMediaPath(string virtualFolderName, MediaPathInfo path); - void UpdateMediaPath(string virtualFolderName, MediaPathInfo path); - void RemoveMediaPath(string virtualFolderName, string path); + + void AddMediaPath(string virtualFolderName, MediaPathInfo mediaPath); + + void UpdateMediaPath(string virtualFolderName, MediaPathInfo mediaPath); + + void RemoveMediaPath(string virtualFolderName, string mediaPath); QueryResult<(BaseItem, ItemCounts)> GetGenres(InternalItemsQuery query); + QueryResult<(BaseItem, ItemCounts)> GetMusicGenres(InternalItemsQuery query); + QueryResult<(BaseItem, ItemCounts)> GetStudios(InternalItemsQuery query); + QueryResult<(BaseItem, ItemCounts)> GetArtists(InternalItemsQuery query); + QueryResult<(BaseItem, ItemCounts)> GetAlbumArtists(InternalItemsQuery query); + QueryResult<(BaseItem, ItemCounts)> GetAllArtists(InternalItemsQuery query); int GetCount(InternalItemsQuery query); - void AddExternalSubtitleStreams(List<MediaStream> streams, + void AddExternalSubtitleStreams( + List<MediaStream> streams, string videoPath, string[] files); + + Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason); + + BaseItem GetParentItem(string parentId, Guid? userId); + + BaseItem GetParentItem(Guid? parentId, Guid? userId); + + /// <summary> + /// Gets or creates a static instance of <see cref="NamingOptions"/>. + /// </summary> + /// <returns>An instance of the <see cref="NamingOptions"/> class.</returns> + NamingOptions GetNamingOptions(); } } diff --git a/MediaBrowser.Controller/Library/ILibraryMonitor.cs b/MediaBrowser.Controller/Library/ILibraryMonitor.cs index 233cfb197..455054bd1 100644 --- a/MediaBrowser.Controller/Library/ILibraryMonitor.cs +++ b/MediaBrowser.Controller/Library/ILibraryMonitor.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.Library diff --git a/MediaBrowser.Controller/Library/ILibraryPostScanTask.cs b/MediaBrowser.Controller/Library/ILibraryPostScanTask.cs index cba5e8fd7..4032e9d83 100644 --- a/MediaBrowser.Controller/Library/ILibraryPostScanTask.cs +++ b/MediaBrowser.Controller/Library/ILibraryPostScanTask.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; namespace MediaBrowser.Controller.Library { /// <summary> - /// An interface for tasks that run after the media library scan + /// An interface for tasks that run after the media library scan. /// </summary> public interface ILibraryPostScanTask { diff --git a/MediaBrowser.Controller/Library/ILiveStream.cs b/MediaBrowser.Controller/Library/ILiveStream.cs index 734932f17..4c44a17fd 100644 --- a/MediaBrowser.Controller/Library/ILiveStream.cs +++ b/MediaBrowser.Controller/Library/ILiveStream.cs @@ -1,3 +1,8 @@ +#nullable disable + +#pragma warning disable CA1711, CS1591 + +using System.IO; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Dto; @@ -6,13 +11,22 @@ namespace MediaBrowser.Controller.Library { public interface ILiveStream { - Task Open(CancellationToken openCancellationToken); - Task Close(); int ConsumerCount { get; set; } + string OriginalStreamId { get; set; } + string TunerHostId { get; } + bool EnableStreamSharing { get; } + MediaSourceInfo MediaSource { get; set; } + string UniqueId { get; } + + Task Open(CancellationToken openCancellationToken); + + Task Close(); + + Stream GetStream(); } } diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs index fbae4edb0..e802796d3 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs @@ -1,8 +1,12 @@ +#nullable disable + +#pragma warning disable CA1002, CS1591 + using System; using System.Collections.Generic; -using System.IO; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Dto; @@ -25,12 +29,14 @@ namespace MediaBrowser.Controller.Library /// <param name="itemId">The item identifier.</param> /// <returns>IEnumerable<MediaStream>.</returns> List<MediaStream> GetMediaStreams(Guid itemId); + /// <summary> /// Gets the media streams. /// </summary> /// <param name="mediaSourceId">The media source identifier.</param> /// <returns>IEnumerable<MediaStream>.</returns> List<MediaStream> GetMediaStreams(string mediaSourceId); + /// <summary> /// Gets the media streams. /// </summary> @@ -39,18 +45,48 @@ namespace MediaBrowser.Controller.Library List<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); + + /// <summary> + /// Gets the media attachments. + /// </summary> + /// <param name="query">The query.</param> + /// <returns>IEnumerable<MediaAttachment>.</returns> + List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query); + + /// <summary> /// Gets the playack media sources. /// </summary> - Task<List<MediaSourceInfo>> GetPlayackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken); + /// <param name="item">Item to use.</param> + /// <param name="user">User to use for operation.</param> + /// <param name="allowMediaProbe">Option to allow media probe.</param> + /// <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); /// <summary> /// Gets the static media sources. /// </summary> + /// <param name="item">Item to use.</param> + /// <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); /// <summary> /// Gets the static media source. /// </summary> + /// <param name="item">Item to use.</param> + /// <param name="mediaSourceId">Media source to get.</param> + /// <param name="liveStreamId">Live stream to use.</param> + /// <param name="enablePathSubstitution">Option to enable path substitution.</param> + /// <param name="cancellationToken">CancellationToken to use for operation.</param> + /// <returns>The static media source wrapped in an awaitable task.</returns> Task<MediaSourceInfo> GetMediaSource(BaseItem item, string mediaSourceId, string liveStreamId, bool enablePathSubstitution, CancellationToken cancellationToken); /// <summary> @@ -74,6 +110,20 @@ namespace MediaBrowser.Controller.Library Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken); /// <summary> + /// Gets the live stream info. + /// </summary> + /// <param name="id">The identifier.</param> + /// <returns>An instance of <see cref="ILiveStream"/>.</returns> + public ILiveStream GetLiveStreamInfo(string id); + + /// <summary> + /// Gets the live stream info using the stream's unique id. + /// </summary> + /// <param name="uniqueId">The unique identifier.</param> + /// <returns>An instance of <see cref="ILiveStream"/>.</returns> + public ILiveStream GetLiveStreamInfoByUniqueId(string uniqueId); + + /// <summary> /// Closes the media source. /// </summary> /// <param name="id">The live stream identifier.</param> @@ -89,12 +139,5 @@ namespace MediaBrowser.Controller.Library void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user); Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProbeDelay, bool isLiveStream, CancellationToken cancellationToken); - - Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken); - } - - public interface IDirectStreamProvider - { - Task CopyToAsync(Stream stream, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Library/IMediaSourceProvider.cs b/MediaBrowser.Controller/Library/IMediaSourceProvider.cs index 9e74879fc..ca4b53fbe 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceProvider.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA1002, CS1591 + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -19,6 +21,10 @@ namespace MediaBrowser.Controller.Library /// <summary> /// Opens the media source. /// </summary> + /// <param name="openToken">Token to use.</param> + /// <param name="currentLiveStreams">List of live streams.</param> + /// <param name="cancellationToken">CancellationToken to use for operation.</param> + /// <returns>The media source wrapped as an awaitable task.</returns> Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Library/IMetadataFileSaver.cs b/MediaBrowser.Controller/Library/IMetadataFileSaver.cs index 5b92388ce..9c6f03a23 100644 --- a/MediaBrowser.Controller/Library/IMetadataFileSaver.cs +++ b/MediaBrowser.Controller/Library/IMetadataFileSaver.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Library diff --git a/MediaBrowser.Controller/Library/IMetadataSaver.cs b/MediaBrowser.Controller/Library/IMetadataSaver.cs index dd119984e..d963fd249 100644 --- a/MediaBrowser.Controller/Library/IMetadataSaver.cs +++ b/MediaBrowser.Controller/Library/IMetadataSaver.cs @@ -1,10 +1,12 @@ +#nullable disable + using System.Threading; using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Library { /// <summary> - /// Interface IMetadataSaver + /// Interface IMetadataSaver. /// </summary> public interface IMetadataSaver { @@ -27,7 +29,6 @@ namespace MediaBrowser.Controller.Library /// </summary> /// <param name="item">The item.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> void Save(BaseItem item, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Library/IMusicManager.cs b/MediaBrowser.Controller/Library/IMusicManager.cs index 554dd0895..ec34a868b 100644 --- a/MediaBrowser.Controller/Library/IMusicManager.cs +++ b/MediaBrowser.Controller/Library/IMusicManager.cs @@ -1,4 +1,9 @@ +#nullable disable + +#pragma warning disable CA1002, CS1591 + using System.Collections.Generic; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -10,16 +15,28 @@ namespace MediaBrowser.Controller.Library /// <summary> /// Gets the instant mix from song. /// </summary> + /// <param name="item">The item to use.</param> + /// <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); /// <summary> /// Gets the instant mix from artist. /// </summary> + /// <param name="artist">The artist to use.</param> + /// <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); /// <summary> /// Gets the instant mix from genre. /// </summary> + /// <param name="genres">The genres to use.</param> + /// <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); } } diff --git a/MediaBrowser.Controller/Library/ISearchEngine.cs b/MediaBrowser.Controller/Library/ISearchEngine.cs index 8498b92ae..31dcbba5b 100644 --- a/MediaBrowser.Controller/Library/ISearchEngine.cs +++ b/MediaBrowser.Controller/Library/ISearchEngine.cs @@ -4,7 +4,7 @@ using MediaBrowser.Model.Search; namespace MediaBrowser.Controller.Library { /// <summary> - /// Interface ILibrarySearchEngine + /// Interface ILibrarySearchEngine. /// </summary> public interface ISearchEngine { diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs index ce4e3f530..034c40591 100644 --- a/MediaBrowser.Controller/Library/IUserDataManager.cs +++ b/MediaBrowser.Controller/Library/IUserDataManager.cs @@ -1,6 +1,11 @@ +#nullable disable + +#pragma warning disable CA1002, CA1707, CS1591 + using System; using System.Collections.Generic; using System.Threading; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; @@ -9,7 +14,7 @@ using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Library { /// <summary> - /// Interface IUserDataManager + /// Interface IUserDataManager. /// </summary> public interface IUserDataManager { @@ -26,41 +31,46 @@ namespace MediaBrowser.Controller.Library /// <param name="userData">The user data.</param> /// <param name="reason">The reason.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> void SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken); - void SaveUserData(User userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken); + + void SaveUserData(User user, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken); UserItemData GetUserData(User user, BaseItem item); - UserItemData GetUserData(string userId, BaseItem item); UserItemData GetUserData(Guid userId, BaseItem item); /// <summary> /// Gets the user data dto. /// </summary> + /// <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, BaseItemDto itemDto, User user, DtoOptions dto_options); + UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto itemDto, User user, DtoOptions options); /// <summary> - /// Get all user data for the given user + /// Get all user data for the given user. /// </summary> - /// <param name="userId"></param> - /// <returns></returns> + /// <param name="userId">The user id.</param> + /// <returns>The user item data.</returns> List<UserItemData> GetAllUserData(Guid userId); /// <summary> - /// Save the all provided user data for the given user + /// Save the all provided user data for the given user. /// </summary> - /// <param name="userId"></param> - /// <param name="userData"></param> - /// <param name="cancellationToken"></param> - /// <returns></returns> + /// <param name="userId">The user id.</param> + /// <param name="userData">The array of user data.</param> + /// <param name="cancellationToken">The cancellation token.</param> void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken); /// <summary> - /// Updates playstate for an item and returns true or false indicating if it was played to completion + /// Updates playstate for an item and returns true or false indicating if it was played to completion. /// </summary> - bool UpdatePlayState(BaseItem item, UserItemData data, long? positionTicks); + /// <param name="item">Item to update.</param> + /// <param name="data">Data to update.</param> + /// <param name="reportedPositionTicks">New playstate.</param> + /// <returns>True if playstate was updated.</returns> + bool UpdatePlayState(BaseItem item, UserItemData data, long? reportedPositionTicks); } } diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index 925d91a37..993e3e18f 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -1,59 +1,55 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Entities; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Events; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Users; namespace MediaBrowser.Controller.Library { /// <summary> - /// Interface IUserManager + /// Interface IUserManager. /// </summary> public interface IUserManager { /// <summary> + /// Occurs when a user is updated. + /// </summary> + event EventHandler<GenericEventArgs<User>> OnUserUpdated; + + /// <summary> /// Gets the users. /// </summary> /// <value>The users.</value> IEnumerable<User> Users { get; } /// <summary> - /// Occurs when [user updated]. + /// Gets the user ids. /// </summary> - event EventHandler<GenericEventArgs<User>> UserUpdated; + /// <value>The users ids.</value> + IEnumerable<Guid> UsersIds { get; } /// <summary> - /// Occurs when [user deleted]. + /// Initializes the user manager and ensures that a user exists. /// </summary> - event EventHandler<GenericEventArgs<User>> UserDeleted; - - event EventHandler<GenericEventArgs<User>> UserCreated; - event EventHandler<GenericEventArgs<User>> UserPolicyUpdated; - event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated; - event EventHandler<GenericEventArgs<User>> UserPasswordChanged; - event EventHandler<GenericEventArgs<User>> UserLockedOut; + /// <returns>Awaitable task.</returns> + Task InitializeAsync(); /// <summary> - /// Gets a User by Id + /// Gets a user by Id. /// </summary> /// <param name="id">The id.</param> - /// <returns>User.</returns> - /// <exception cref="ArgumentNullException"></exception> + /// <returns>The user with the specified Id, or <c>null</c> if the user doesn't exist.</returns> + /// <exception cref="ArgumentException"><c>id</c> is an empty Guid.</exception> User GetUserById(Guid id); /// <summary> - /// Gets the user by identifier. - /// </summary> - /// <param name="id">The identifier.</param> - /// <returns>User.</returns> - User GetUserById(string id); - - /// <summary> /// Gets the name of the user by. /// </summary> /// <param name="name">The name.</param> @@ -61,47 +57,39 @@ namespace MediaBrowser.Controller.Library User GetUserByName(string name); /// <summary> - /// Refreshes metadata for each user - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - Task RefreshUsersMetadata(CancellationToken cancellationToken); - - /// <summary> /// Renames the user. /// </summary> /// <param name="user">The user.</param> /// <param name="newName">The new name.</param> /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException">user</exception> - /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentNullException">If user is <c>null</c>.</exception> + /// <exception cref="ArgumentException">If the provided user doesn't exist.</exception> Task RenameUser(User user, string newName); /// <summary> /// Updates the user. /// </summary> /// <param name="user">The user.</param> - /// <exception cref="ArgumentNullException">user</exception> - /// <exception cref="ArgumentException"></exception> - void UpdateUser(User user); + /// <exception cref="ArgumentNullException">If user is <c>null</c>.</exception> + /// <exception cref="ArgumentException">If the provided user doesn't exist.</exception> + /// <returns>A task representing the update of the user.</returns> + Task UpdateUserAsync(User user); /// <summary> - /// Creates the user. + /// Creates a user with the specified name. /// </summary> - /// <param name="name">The name.</param> - /// <returns>User.</returns> - /// <exception cref="ArgumentNullException">name</exception> - /// <exception cref="ArgumentException"></exception> - Task<User> CreateUser(string name); + /// <param name="name">The name of the new user.</param> + /// <returns>The created user.</returns> + /// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c> or empty.</exception> + /// <exception cref="ArgumentException"><paramref name="name"/> already exists.</exception> + Task<User> CreateUserAsync(string name); /// <summary> - /// Deletes the user. + /// Deletes the specified user. /// </summary> - /// <param name="user">The user.</param> - /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException">user</exception> - /// <exception cref="ArgumentException"></exception> - Task DeleteUser(User user); + /// <param name="userId">The id of the user to be deleted.</param> + /// <returns>A task representing the deletion of the user.</returns> + Task DeleteUserAsync(Guid userId); /// <summary> /// Resets the password. @@ -111,28 +99,28 @@ namespace MediaBrowser.Controller.Library Task ResetPassword(User user); /// <summary> - /// Gets the offline user dto. - /// </summary> - /// <param name="user">The user.</param> - /// <returns>UserDto.</returns> - UserDto GetOfflineUserDto(User user); - - /// <summary> /// Resets the easy password. /// </summary> /// <param name="user">The user.</param> /// <returns>Task.</returns> - void ResetEasyPassword(User user); + Task ResetEasyPassword(User user); /// <summary> /// Changes the password. /// </summary> + /// <param name="user">The user.</param> + /// <param name="newPassword">New password to use.</param> + /// <returns>Awaitable task.</returns> Task ChangePassword(User user, string newPassword); /// <summary> /// Changes the easy password. /// </summary> - void ChangeEasyPassword(User user, string newPassword, string newPasswordSha1); + /// <param name="user">The user.</param> + /// <param name="newPassword">New password to use.</param> + /// <param name="newPasswordSha1">Hash of new password.</param> + /// <returns>Task.</returns> + Task ChangeEasyPassword(User user, string newPassword, string newPasswordSha1); /// <summary> /// Gets the user dto. @@ -145,6 +133,12 @@ namespace MediaBrowser.Controller.Library /// <summary> /// Authenticates the user. /// </summary> + /// <param name="username">The user.</param> + /// <param name="password">The password to use.</param> + /// <param name="passwordSha1">Hash of password.</param> + /// <param name="remoteEndPoint">Remove endpoint to use.</param> + /// <param name="isUserSession">Specifies if a user session.</param> + /// <returns>User wrapped in awaitable task.</returns> Task<User> AuthenticateUser(string username, string password, string passwordSha1, string remoteEndPoint, bool isUserSession); /// <summary> @@ -162,46 +156,35 @@ namespace MediaBrowser.Controller.Library /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> Task<PinRedeemResult> RedeemPasswordResetPin(string pin); - /// <summary> - /// Gets the user policy. - /// </summary> - /// <param name="user">The user.</param> - /// <returns>UserPolicy.</returns> - UserPolicy GetUserPolicy(User user); + NameIdPair[] GetAuthenticationProviders(); - /// <summary> - /// Gets the user configuration. - /// </summary> - /// <param name="user">The user.</param> - /// <returns>UserConfiguration.</returns> - UserConfiguration GetUserConfiguration(User user); + NameIdPair[] GetPasswordResetProviders(); /// <summary> - /// Updates the configuration. + /// This method updates the user's configuration. + /// This is only included as a stopgap until the new API, using this internally is not recommended. + /// Instead, modify the user object directly, then call <see cref="UpdateUserAsync"/>. /// </summary> - /// <param name="userId">The user identifier.</param> - /// <param name="newConfiguration">The new configuration.</param> - /// <returns>Task.</returns> - void UpdateConfiguration(Guid userId, UserConfiguration newConfiguration); - - void UpdateConfiguration(User user, UserConfiguration newConfiguration); + /// <param name="userId">The user's Id.</param> + /// <param name="config">The request containing the new user configuration.</param> + /// <returns>A task representing the update.</returns> + Task UpdateConfigurationAsync(Guid userId, UserConfiguration config); /// <summary> - /// Updates the user policy. + /// This method updates the user's policy. + /// This is only included as a stopgap until the new API, using this internally is not recommended. + /// Instead, modify the user object directly, then call <see cref="UpdateUserAsync"/>. /// </summary> - /// <param name="userId">The user identifier.</param> - /// <param name="userPolicy">The user policy.</param> - void UpdateUserPolicy(Guid userId, UserPolicy userPolicy); + /// <param name="userId">The user's Id.</param> + /// <param name="policy">The request containing the new user policy.</param> + /// <returns>A task representing the update.</returns> + Task UpdatePolicyAsync(Guid userId, UserPolicy policy); /// <summary> - /// Makes the valid username. + /// Clears the user's profile image. /// </summary> - /// <param name="username">The username.</param> - /// <returns>System.String.</returns> - string MakeValidUsername(string username); - - void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders); - - NameIdPair[] GetAuthenticationProviders(); + /// <param name="user">The user.</param> + /// <returns>A task representing the clearing of the profile image.</returns> + Task ClearProfileImageAsync(User user); } } diff --git a/MediaBrowser.Controller/Library/IUserViewManager.cs b/MediaBrowser.Controller/Library/IUserViewManager.cs index 0d7da7579..055627d3e 100644 --- a/MediaBrowser.Controller/Library/IUserViewManager.cs +++ b/MediaBrowser.Controller/Library/IUserViewManager.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CA1002, CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Controller.Dto; @@ -9,9 +13,29 @@ namespace MediaBrowser.Controller.Library { public interface IUserViewManager { + /// <summary> + /// Gets user views. + /// </summary> + /// <param name="query">Query to use.</param> + /// <returns>Set of folders.</returns> Folder[] GetUserViews(UserViewQuery query); + + /// <summary> + /// Gets user sub views. + /// </summary> + /// <param name="parentId">Parent to use.</param> + /// <param name="type">Type to use.</param> + /// <param name="localizationKey">Localization key to use.</param> + /// <param name="sortName">Sort to use.</param> + /// <returns>User view.</returns> UserView GetUserSubView(Guid parentId, string type, string localizationKey, string sortName); + /// <summary> + /// Gets latest items. + /// </summary> + /// <param name="request">Query to use.</param> + /// <param name="options">Options to use.</param> + /// <returns>Set of items.</returns> List<Tuple<BaseItem, List<BaseItem>>> GetLatestItems(LatestItemsQuery request, DtoOptions options); } } diff --git a/MediaBrowser.Controller/Library/IntroInfo.cs b/MediaBrowser.Controller/Library/IntroInfo.cs index 0e761d549..90786786b 100644 --- a/MediaBrowser.Controller/Library/IntroInfo.cs +++ b/MediaBrowser.Controller/Library/IntroInfo.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.Library diff --git a/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs b/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs index c9671de47..3586dc69d 100644 --- a/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs +++ b/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs @@ -1,9 +1,13 @@ +#nullable disable + +#pragma warning disable CA1711, CS1591 + using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Library { /// <summary> - /// Class ItemChangeEventArgs + /// Class ItemChangeEventArgs. /// </summary> public class ItemChangeEventArgs { diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs index 0222b926e..bfc1e4857 100644 --- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs +++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CA1721, CA1819, CS1591 + using System; using System.Collections.Generic; using System.Linq; @@ -12,14 +16,14 @@ namespace MediaBrowser.Controller.Library /// These are arguments relating to the file system that are collected once and then referred to /// whenever needed. Primarily for entity resolution. /// </summary> - public class ItemResolveArgs : EventArgs + public class ItemResolveArgs { /// <summary> - /// The _app paths + /// The _app paths. /// </summary> private readonly IServerApplicationPaths _appPaths; - public IDirectoryService DirectoryService { get; private set; } + private LibraryOptions _libraryOptions; /// <summary> /// Initializes a new instance of the <see cref="ItemResolveArgs" /> class. @@ -32,17 +36,18 @@ namespace MediaBrowser.Controller.Library DirectoryService = directoryService; } + public IDirectoryService DirectoryService { get; } + /// <summary> - /// Gets the file system children. + /// Gets or sets the file system children. /// </summary> /// <value>The file system children.</value> public FileSystemMetadata[] FileSystemChildren { get; set; } - public LibraryOptions LibraryOptions { get; set; } - - public LibraryOptions GetLibraryOptions() + public LibraryOptions LibraryOptions { - return LibraryOptions ?? (LibraryOptions = (Parent == null ? new LibraryOptions() : BaseItem.LibraryManager.GetLibraryOptions(Parent))); + get => _libraryOptions ??= Parent == null ? new LibraryOptions() : BaseItem.LibraryManager.GetLibraryOptions(Parent); + set => _libraryOptions = value; } /// <summary> @@ -58,10 +63,10 @@ namespace MediaBrowser.Controller.Library public FileSystemMetadata FileInfo { get; set; } /// <summary> - /// Gets or sets the path. + /// Gets the path. /// </summary> /// <value>The path.</value> - public string Path { get; set; } + public string Path => FileInfo.FullName; /// <summary> /// Gets a value indicating whether this instance is directory. @@ -89,7 +94,6 @@ namespace MediaBrowser.Controller.Library return parentDir.Length > _appPaths.RootFolderPath.Length && parentDir.StartsWith(_appPaths.RootFolderPath, StringComparison.OrdinalIgnoreCase); - } } @@ -105,6 +109,21 @@ namespace MediaBrowser.Controller.Library /// <value>The additional locations.</value> private List<string> AdditionalLocations { get; set; } + /// <summary> + /// Gets the physical locations. + /// </summary> + /// <value>The physical locations.</value> + public string[] PhysicalLocations + { + get + { + var paths = string.IsNullOrEmpty(Path) ? Array.Empty<string>() : new[] { Path }; + return AdditionalLocations == null ? paths : paths.Concat(AdditionalLocations).ToArray(); + } + } + + public string CollectionType { get; set; } + public bool HasParent<T>() where T : Folder { @@ -129,16 +148,26 @@ namespace MediaBrowser.Controller.Library } return item != null; - } + return false; } /// <summary> + /// Determines whether the specified <see cref="object" /> is equal to this instance. + /// </summary> + /// <param name="obj">The object to compare with the current object.</param> + /// <returns><c>true</c> if the specified <see cref="object" /> is equal to this instance; otherwise, <c>false</c>.</returns> + public override bool Equals(object obj) + { + return Equals(obj as ItemResolveArgs); + } + + /// <summary> /// Adds the additional location. /// </summary> /// <param name="path">The path.</param> - /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c> or empty.</exception> public void AddAdditionalLocation(string path) { if (string.IsNullOrEmpty(path)) @@ -146,34 +175,18 @@ namespace MediaBrowser.Controller.Library throw new ArgumentException("The path was empty or null.", nameof(path)); } - if (AdditionalLocations == null) - { - AdditionalLocations = new List<string>(); - } - + AdditionalLocations ??= new List<string>(); AdditionalLocations.Add(path); } // REVIEW: @bond - /// <summary> - /// Gets the physical locations. - /// </summary> - /// <value>The physical locations.</value> - public string[] PhysicalLocations - { - get - { - var paths = string.IsNullOrEmpty(Path) ? Array.Empty<string>() : new[] { Path }; - return AdditionalLocations == null ? paths : paths.Concat(AdditionalLocations).ToArray(); - } - } /// <summary> /// Gets the name of the file system entry by. /// </summary> /// <param name="name">The name.</param> /// <returns>FileSystemInfo.</returns> - /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c> or empty.</exception> public FileSystemMetadata GetFileSystemEntryByName(string name) { if (string.IsNullOrEmpty(name)) @@ -189,7 +202,7 @@ namespace MediaBrowser.Controller.Library /// </summary> /// <param name="path">The path.</param> /// <returns>FileSystemInfo.</returns> - /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentNullException">Throws if path is invalid.</exception> public FileSystemMetadata GetFileSystemEntryByPath(string path) { if (string.IsNullOrEmpty(path)) @@ -223,45 +236,33 @@ namespace MediaBrowser.Controller.Library return CollectionType; } - public string CollectionType { get; set; } - - #region Equality Overrides - - /// <summary> - /// Determines whether the specified <see cref="object" /> is equal to this instance. - /// </summary> - /// <param name="obj">The object to compare with the current object.</param> - /// <returns><c>true</c> if the specified <see cref="object" /> is equal to this instance; otherwise, <c>false</c>.</returns> - public override bool Equals(object obj) - { - return Equals(obj as ItemResolveArgs); - } - /// <summary> /// Returns a hash code for this instance. /// </summary> /// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.</returns> public override int GetHashCode() { - return Path.GetHashCode(); + return Path.GetHashCode(StringComparison.Ordinal); } /// <summary> /// Equals the specified args. /// </summary> /// <param name="args">The args.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> + /// <returns><c>true</c> if the arguments are the same, <c>false</c> otherwise.</returns> protected bool Equals(ItemResolveArgs args) { if (args != null) { - if (args.Path == null && Path == null) return true; + if (args.Path == null && Path == null) + { + return true; + } + return args.Path != null && BaseItem.FileSystem.AreEqual(args.Path, Path); } + return false; } - - #endregion } - } diff --git a/MediaBrowser.Controller/Library/ItemUpdateType.cs b/MediaBrowser.Controller/Library/ItemUpdateType.cs index b62f314ba..1f3ebb499 100644 --- a/MediaBrowser.Controller/Library/ItemUpdateType.cs +++ b/MediaBrowser.Controller/Library/ItemUpdateType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.Library diff --git a/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs index 037b0b62c..7bc8fa5ab 100644 --- a/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs +++ b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs b/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs new file mode 100644 index 000000000..41cfcae16 --- /dev/null +++ b/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs @@ -0,0 +1,17 @@ +#nullable disable + +#pragma warning disable CS1591 + +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Configuration; + +namespace MediaBrowser.Controller.Library +{ + public static class MetadataConfigurationExtensions + { + public static MetadataConfiguration GetMetadataConfiguration(this IConfigurationManager config) + { + return config.GetConfiguration<MetadataConfiguration>("metadata"); + } + } +} diff --git a/MediaBrowser.Controller/Library/MetadataConfigurationStore.cs b/MediaBrowser.Controller/Library/MetadataConfigurationStore.cs index 31adbdcf3..a6be6c0d3 100644 --- a/MediaBrowser.Controller/Library/MetadataConfigurationStore.cs +++ b/MediaBrowser.Controller/Library/MetadataConfigurationStore.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Configuration; @@ -12,18 +14,10 @@ namespace MediaBrowser.Controller.Library { new ConfigurationStore { - Key = "metadata", - ConfigurationType = typeof(MetadataConfiguration) + Key = "metadata", + ConfigurationType = typeof(MetadataConfiguration) } }; } } - - public static class MetadataConfigurationExtensions - { - public static MetadataConfiguration GetMetadataConfiguration(this IConfigurationManager config) - { - return config.GetConfiguration<MetadataConfiguration>("metadata"); - } - } } diff --git a/MediaBrowser.Controller/Library/NameExtensions.cs b/MediaBrowser.Controller/Library/NameExtensions.cs index 6b0b7e53a..d2ed3465a 100644 --- a/MediaBrowser.Controller/Library/NameExtensions.cs +++ b/MediaBrowser.Controller/Library/NameExtensions.cs @@ -1,13 +1,19 @@ +#pragma warning disable CS1591 + using System; -using System.Linq; using System.Collections.Generic; -using MediaBrowser.Controller.Extensions; +using System.Linq; +using Diacritics.Extensions; namespace MediaBrowser.Controller.Library { public static class NameExtensions { - private static string RemoveDiacritics(string name) + public static IEnumerable<string> DistinctNames(this IEnumerable<string> names) + => names.GroupBy(RemoveDiacritics, StringComparer.OrdinalIgnoreCase) + .Select(x => x.First()); + + private static string RemoveDiacritics(string? name) { if (name == null) { @@ -16,9 +22,5 @@ namespace MediaBrowser.Controller.Library return name.RemoveDiacritics(); } - - public static IEnumerable<string> DistinctNames(this IEnumerable<string> names) - => names.GroupBy(RemoveDiacritics, StringComparer.OrdinalIgnoreCase) - .Select(x => x.First()); } } diff --git a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs index b0302d04c..76e9eb1f5 100644 --- a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs +++ b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs @@ -1,5 +1,10 @@ +#nullable disable + +#pragma warning disable CA1002, CA2227, CS1591 + using System; using System.Collections.Generic; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; @@ -7,28 +12,37 @@ using MediaBrowser.Model.Dto; namespace MediaBrowser.Controller.Library { /// <summary> - /// Holds information about a playback progress event + /// Holds information about a playback progress event. /// </summary> public class PlaybackProgressEventArgs : EventArgs { + public PlaybackProgressEventArgs() + { + Users = new List<User>(); + } + public List<User> Users { get; set; } + public long? PlaybackPositionTicks { get; set; } + public BaseItem Item { get; set; } + public BaseItemDto MediaInfo { get; set; } + public string MediaSourceId { get; set; } + public bool IsPaused { get; set; } + public bool IsAutomated { get; set; } public string DeviceId { get; set; } + public string DeviceName { get; set; } + public string ClientName { get; set; } public string PlaySessionId { get; set; } - public SessionInfo Session { get; set; } - public PlaybackProgressEventArgs() - { - Users = new List<User>(); - } + public SessionInfo Session { get; set; } } } diff --git a/MediaBrowser.Controller/Library/PlaybackStartEventArgs.cs b/MediaBrowser.Controller/Library/PlaybackStartEventArgs.cs new file mode 100644 index 000000000..2138fef58 --- /dev/null +++ b/MediaBrowser.Controller/Library/PlaybackStartEventArgs.cs @@ -0,0 +1,9 @@ +namespace MediaBrowser.Controller.Library +{ + /// <summary> + /// An event that occurs when playback is started. + /// </summary> + public class PlaybackStartEventArgs : PlaybackProgressEventArgs + { + } +} diff --git a/MediaBrowser.Controller/Library/PlaybackStopEventArgs.cs b/MediaBrowser.Controller/Library/PlaybackStopEventArgs.cs index 12add2573..f0d77ba2d 100644 --- a/MediaBrowser.Controller/Library/PlaybackStopEventArgs.cs +++ b/MediaBrowser.Controller/Library/PlaybackStopEventArgs.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Library { public class PlaybackStopEventArgs : PlaybackProgressEventArgs diff --git a/MediaBrowser.Controller/Library/Profiler.cs b/MediaBrowser.Controller/Library/Profiler.cs deleted file mode 100644 index 9fe175a7c..000000000 --- a/MediaBrowser.Controller/Library/Profiler.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Diagnostics; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Controller.Library -{ - /// <summary> - /// Class Profiler - /// </summary> - public class Profiler : IDisposable - { - /// <summary> - /// The name - /// </summary> - readonly string _name; - /// <summary> - /// The stopwatch - /// </summary> - readonly Stopwatch _stopwatch; - - /// <summary> - /// The _logger - /// </summary> - private readonly ILogger _logger; - - /// <summary> - /// Initializes a new instance of the <see cref="Profiler" /> class. - /// </summary> - /// <param name="name">The name.</param> - /// <param name="logger">The logger.</param> - public Profiler(string name, ILogger logger) - { - this._name = name; - - _logger = logger; - - _stopwatch = new Stopwatch(); - _stopwatch.Start(); - } - #region IDisposable Members - - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> - public void Dispose() - { - Dispose(true); - } - - /// <summary> - /// Releases unmanaged and - optionally - managed resources. - /// </summary> - /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool dispose) - { - if (dispose) - { - _stopwatch.Stop(); - string message; - if (_stopwatch.ElapsedMilliseconds > 300000) - { - message = string.Format("{0} took {1} minutes.", - _name, ((float)_stopwatch.ElapsedMilliseconds / 60000).ToString("F")); - } - else - { - message = string.Format("{0} took {1} seconds.", - _name, ((float)_stopwatch.ElapsedMilliseconds / 1000).ToString("#0.000")); - } - _logger.LogInformation(message); - } - } - - #endregion - } -} diff --git a/MediaBrowser.Controller/Library/SearchHintInfo.cs b/MediaBrowser.Controller/Library/SearchHintInfo.cs index 692431e34..de7806adc 100644 --- a/MediaBrowser.Controller/Library/SearchHintInfo.cs +++ b/MediaBrowser.Controller/Library/SearchHintInfo.cs @@ -1,9 +1,11 @@ +#nullable disable + using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Library { /// <summary> - /// Class SearchHintInfo + /// Class SearchHintInfo. /// </summary> public class SearchHintInfo { diff --git a/MediaBrowser.Controller/Library/TVUtils.cs b/MediaBrowser.Controller/Library/TVUtils.cs index fd5fb6748..968338dc6 100644 --- a/MediaBrowser.Controller/Library/TVUtils.cs +++ b/MediaBrowser.Controller/Library/TVUtils.cs @@ -1,9 +1,10 @@ using System; +using System.Diagnostics.CodeAnalysis; namespace MediaBrowser.Controller.Library { /// <summary> - /// Class TVUtils + /// Class TVUtils. /// </summary> public static class TVUtils { @@ -12,7 +13,8 @@ namespace MediaBrowser.Controller.Library /// </summary> /// <param name="day">The day.</param> /// <returns>List{DayOfWeek}.</returns> - public static DayOfWeek[] GetAirDays(string day) + [return: NotNullIfNotNull("day")] + public static DayOfWeek[]? GetAirDays(string? day) { if (!string.IsNullOrEmpty(day)) { @@ -38,8 +40,9 @@ namespace MediaBrowser.Controller.Library }; } - return new DayOfWeek[] { }; + return Array.Empty<DayOfWeek>(); } + return null; } } diff --git a/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs b/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs index 3e7351b8b..4d90346f2 100644 --- a/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs +++ b/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CA1002, CA2227, CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Controller.Entities; @@ -6,7 +10,7 @@ using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Library { /// <summary> - /// Class UserDataSaveEventArgs + /// Class UserDataSaveEventArgs. /// </summary> public class UserDataSaveEventArgs : EventArgs { diff --git a/MediaBrowser.Controller/LiveTv/ActiveRecordingInfo.cs b/MediaBrowser.Controller/LiveTv/ActiveRecordingInfo.cs new file mode 100644 index 000000000..463061e68 --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/ActiveRecordingInfo.cs @@ -0,0 +1,19 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System.Threading; + +namespace MediaBrowser.Controller.LiveTv +{ + public class ActiveRecordingInfo + { + public string Id { get; set; } + + public string Path { get; set; } + + public TimerInfo Timer { get; set; } + + public CancellationTokenSource CancellationTokenSource { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs index 70477fce7..699c15f93 100644 --- a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs +++ b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs @@ -1,9 +1,13 @@ +#nullable disable + +#pragma warning disable CS1591 + using MediaBrowser.Model.LiveTv; namespace MediaBrowser.Controller.LiveTv { /// <summary> - /// Class ChannelInfo + /// Class ChannelInfo. /// </summary> public class ChannelInfo { @@ -20,7 +24,7 @@ namespace MediaBrowser.Controller.LiveTv public string Number { get; set; } /// <summary> - /// Get or sets the Id. + /// Gets or sets the Id. /// </summary> /// <value>The id of the channel.</value> public string Id { get; set; } @@ -44,13 +48,19 @@ namespace MediaBrowser.Controller.LiveTv public ChannelType ChannelType { get; set; } /// <summary> - /// Supply the image path if it can be accessed directly from the file system + /// Gets or sets the group of the channel. + /// </summary> + /// <value>The group of the channel.</value> + public string ChannelGroup { get; set; } + + /// <summary> + /// Gets or sets the the image path if it can be accessed directly from the file system. /// </summary> /// <value>The image path.</value> public string ImagePath { get; set; } /// <summary> - /// Supply the image url if it can be downloaded + /// Gets or sets the image url if it can be downloaded. /// </summary> /// <value>The image URL.</value> public string ImageUrl { get; set; } @@ -60,6 +70,7 @@ namespace MediaBrowser.Controller.LiveTv /// </summary> /// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value> public bool? HasImage { get; set; } + /// <summary> /// Gets or sets a value indicating whether this instance is favorite. /// </summary> @@ -67,8 +78,11 @@ namespace MediaBrowser.Controller.LiveTv public bool? IsFavorite { get; set; } public bool? IsHD { get; set; } + public string AudioCodec { get; set; } + public string VideoCodec { get; set; } + public string[] Tags { get; set; } } } diff --git a/MediaBrowser.Controller/LiveTv/IListingsProvider.cs b/MediaBrowser.Controller/LiveTv/IListingsProvider.cs index c719ad81c..2bd4b20e8 100644 --- a/MediaBrowser.Controller/LiveTv/IListingsProvider.cs +++ b/MediaBrowser.Controller/LiveTv/IListingsProvider.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading; @@ -10,10 +14,15 @@ namespace MediaBrowser.Controller.LiveTv public interface IListingsProvider { string Name { get; } + string Type { get; } + Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken); + Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings); + Task<List<NameIdPair>> GetLineups(ListingsProviderInfo info, string country, string location); + Task<List<ChannelInfo>> GetChannels(ListingsProviderInfo info, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index f99df6c7c..dbd18165d 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -1,28 +1,43 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Events; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Events; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; namespace MediaBrowser.Controller.LiveTv { /// <summary> - /// Manages all live tv services installed on the server + /// Manages all live tv services installed on the server. /// </summary> public interface ILiveTvManager { + event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled; + + event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled; + + event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCreated; + + event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCreated; + /// <summary> /// Gets the services. /// </summary> /// <value>The services.</value> IReadOnlyList<ILiveTvService> Services { get; } + IListingsProvider[] ListingProviders { get; } + /// <summary> /// Gets the new timer defaults asynchronous. /// </summary> @@ -81,6 +96,7 @@ namespace MediaBrowser.Controller.LiveTv /// </summary> /// <param name="query">The query.</param> /// <param name="options">The options.</param> + /// <returns>A recording.</returns> QueryResult<BaseItemDto> GetRecordings(RecordingQuery query, DtoOptions options); /// <summary> @@ -104,6 +120,7 @@ namespace MediaBrowser.Controller.LiveTv /// </summary> /// <param name="id">The identifier.</param> /// <param name="mediaSourceId">The media source identifier.</param> + /// <param name="currentLiveStreams">The current live streams.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{StreamResponseInfo}.</returns> Task<Tuple<MediaSourceInfo, ILiveStream>> GetChannelStream(string id, string mediaSourceId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken); @@ -170,11 +187,16 @@ namespace MediaBrowser.Controller.LiveTv /// <param name="query">The query.</param> /// <param name="options">The options.</param> /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Recommended programs.</returns> QueryResult<BaseItemDto> GetRecommendedPrograms(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken); /// <summary> /// Gets the recommended programs internal. /// </summary> + /// <param name="query">The query.</param> + /// <param name="options">The options.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Recommended programs.</returns> QueryResult<BaseItem> GetRecommendedProgramsInternal(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken); /// <summary> @@ -196,6 +218,7 @@ namespace MediaBrowser.Controller.LiveTv /// Gets the live tv folder. /// </summary> /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Live TV folder.</returns> Folder GetInternalLiveTvFolder(CancellationToken cancellationToken); /// <summary> @@ -207,11 +230,18 @@ namespace MediaBrowser.Controller.LiveTv /// <summary> /// Gets the internal channels. /// </summary> + /// <param name="query">The query.</param> + /// <param name="dtoOptions">The options.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Internal channels.</returns> QueryResult<BaseItem> GetInternalChannels(LiveTvChannelQuery query, DtoOptions dtoOptions, CancellationToken cancellationToken); /// <summary> /// Gets the channel media sources. /// </summary> + /// <param name="item">Item to search for.</param> + /// <param name="cancellationToken">CancellationToken to use for operation.</param> + /// <returns>Channel media sources wrapped in a task.</returns> Task<IEnumerable<MediaSourceInfo>> GetChannelMediaSources(BaseItem item, CancellationToken cancellationToken); /// <summary> @@ -221,12 +251,16 @@ namespace MediaBrowser.Controller.LiveTv /// <param name="fields">The fields.</param> /// <param name="user">The user.</param> /// <returns>Task.</returns> - Task AddInfoToProgramDto(List<Tuple<BaseItem, BaseItemDto>> programs, ItemFields[] fields, User user = null); + Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> programs, IReadOnlyList<ItemFields> fields, User user = null); /// <summary> /// Saves the tuner host. /// </summary> + /// <param name="info">Turner host to save.</param> + /// <param name="dataSourceChanged">Option to specify that data source has changed.</param> + /// <returns>Tuner host information wrapped in a task.</returns> Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true); + /// <summary> /// Saves the listing provider. /// </summary> @@ -240,7 +274,7 @@ namespace MediaBrowser.Controller.LiveTv Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber); - TunerChannelMapping GetTunerChannelMapping(ChannelInfo channel, NameValuePair[] mappings, List<ChannelInfo> providerChannels); + TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, List<ChannelInfo> providerChannels); /// <summary> /// Gets the lineups. @@ -258,20 +292,15 @@ namespace MediaBrowser.Controller.LiveTv /// <param name="items">The items.</param> /// <param name="options">The options.</param> /// <param name="user">The user.</param> - void AddChannelInfo(List<Tuple<BaseItemDto, LiveTvChannel>> items, DtoOptions options, User user); + void AddChannelInfo(IReadOnlyCollection<(BaseItemDto, LiveTvChannel)> items, DtoOptions options, User user); Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken); - Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken); - IListingsProvider[] ListingProviders { get; } + Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken); List<NameIdPair> GetTunerHostTypes(); - Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken); - event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled; - event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled; - event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCreated; - event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCreated; + Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken); string GetEmbyTvActiveRecordingPath(string id); @@ -281,12 +310,4 @@ namespace MediaBrowser.Controller.LiveTv List<BaseItem> GetRecordingFolders(User user); } - - public class ActiveRecordingInfo - { - public string Id { get; set; } - public string Path { get; set; } - public TimerInfo Timer { get; set; } - public CancellationTokenSource CancellationTokenSource { get; set; } - } } diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs index b71a76648..ce34954e3 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading; @@ -66,10 +70,10 @@ namespace MediaBrowser.Controller.LiveTv /// <summary> /// Updates the timer asynchronous. /// </summary> - /// <param name="info">The information.</param> + /// <param name="updatedTimer">The updated timer information.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - Task UpdateTimerAsync(TimerInfo info, CancellationToken cancellationToken); + Task UpdateTimerAsync(TimerInfo updatedTimer, CancellationToken cancellationToken); /// <summary> /// Updates the series timer asynchronous. diff --git a/MediaBrowser.Controller/LiveTv/ITunerHost.cs b/MediaBrowser.Controller/LiveTv/ITunerHost.cs index 240ba8c23..24820abb9 100644 --- a/MediaBrowser.Controller/LiveTv/ITunerHost.cs +++ b/MediaBrowser.Controller/LiveTv/ITunerHost.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -14,28 +18,40 @@ namespace MediaBrowser.Controller.LiveTv /// </summary> /// <value>The name.</value> string Name { get; } + /// <summary> /// Gets the type. /// </summary> /// <value>The type.</value> string Type { get; } + + bool IsSupported { get; } + /// <summary> /// Gets the channels. /// </summary> + /// <param name="enableCache">Option to enable using cache.</param> + /// <param name="cancellationToken">The CancellationToken for this operation.</param> /// <returns>Task<IEnumerable<ChannelInfo>>.</returns> Task<List<ChannelInfo>> GetChannels(bool enableCache, CancellationToken cancellationToken); + /// <summary> /// Gets the tuner infos. /// </summary> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task<List<LiveTvTunerInfo>>.</returns> Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken); + /// <summary> /// Gets the channel stream. /// </summary> /// <param name="channelId">The channel identifier.</param> /// <param name="streamId">The stream identifier.</param> + /// <param name="currentLiveStreams">The current live streams.</param> + /// <param name="cancellationToken">The cancellation token to cancel operation.</param> + /// <returns>Live stream wrapped in a task.</returns> Task<ILiveStream> GetChannelStream(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken); + /// <summary> /// Gets the channel stream media sources. /// </summary> @@ -45,11 +61,8 @@ namespace MediaBrowser.Controller.LiveTv Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken); Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken); - bool IsSupported - { - get; - } } + public interface IConfigurableTunerHost { /// <summary> diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs index 55f47aae9..074e023e8 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs @@ -1,43 +1,30 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.LiveTv { public class LiveTvChannel : BaseItem, IHasMediaSources, IHasProgramAttributes { - public override List<string> GetUserDataKeys() - { - var list = base.GetUserDataKeys(); - - if (!ConfigurationManager.Configuration.DisableLiveTvChannelUserDataName) - { - list.Insert(0, GetClientTypeName() + "-" + Name); - } - - return list; - } - - public override UnratedItem GetBlockUnratedType() - { - return UnratedItem.LiveTvChannel; - } - - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPositionTicksResume => false; - [IgnoreDataMember] + [JsonIgnore] public override SourceType SourceType => SourceType.LiveTV; - [IgnoreDataMember] + [JsonIgnore] public override bool EnableRememberingTrackSelections => false; /// <summary> @@ -52,9 +39,70 @@ namespace MediaBrowser.Controller.LiveTv /// <value>The type of the channel.</value> public ChannelType ChannelType { get; set; } - [IgnoreDataMember] + [JsonIgnore] public override LocationType LocationType => LocationType.Remote; + [JsonIgnore] + public override string MediaType => ChannelType == ChannelType.Radio ? Model.Entities.MediaType.Audio : Model.Entities.MediaType.Video; + + [JsonIgnore] + public bool IsMovie { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is sports. + /// </summary> + /// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value> + [JsonIgnore] + public bool IsSports { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is series. + /// </summary> + /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value> + [JsonIgnore] + public bool IsSeries { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is news. + /// </summary> + /// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value> + [JsonIgnore] + public bool IsNews { get; set; } + + /// <summary> + /// Gets a value indicating whether this instance is kids. + /// </summary> + /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value> + [JsonIgnore] + public bool IsKids => Tags.Contains("Kids", StringComparer.OrdinalIgnoreCase); + + [JsonIgnore] + public bool IsRepeat { get; set; } + + /// <summary> + /// Gets or sets the episode title. + /// </summary> + /// <value>The episode title.</value> + [JsonIgnore] + public string EpisodeTitle { get; set; } + + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + if (!ConfigurationManager.Configuration.DisableLiveTvChannelUserDataName) + { + list.Insert(0, GetClientTypeName() + "-" + Name); + } + + return list; + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.LiveTvChannel; + } + protected override string CreateSortName() { if (!string.IsNullOrEmpty(Number)) @@ -63,22 +111,19 @@ namespace MediaBrowser.Controller.LiveTv if (double.TryParse(Number, NumberStyles.Any, CultureInfo.InvariantCulture, out number)) { - return string.Format("{0:00000.0}", number) + "-" + (Name ?? string.Empty); + return string.Format(CultureInfo.InvariantCulture, "{0:00000.0}", number) + "-" + (Name ?? string.Empty); } } return (Number ?? string.Empty) + "-" + (Name ?? string.Empty); } - [IgnoreDataMember] - public override string MediaType => ChannelType == ChannelType.Radio ? Model.Entities.MediaType.Audio : Model.Entities.MediaType.Video; - public override string GetClientTypeName() { return "TvChannel"; } - public IEnumerable<BaseItem> GetTaggedItems(IEnumerable<BaseItem> inputItems) + public IEnumerable<BaseItem> GetTaggedItems() { return new List<BaseItem>(); } @@ -89,7 +134,7 @@ namespace MediaBrowser.Controller.LiveTv var info = new MediaSourceInfo { - Id = Id.ToString("N"), + Id = Id.ToString("N", CultureInfo.InvariantCulture), Protocol = PathProtocol ?? MediaProtocol.File, MediaStreams = new List<MediaStream>(), Name = Name, @@ -111,53 +156,12 @@ namespace MediaBrowser.Controller.LiveTv protected override string GetInternalMetadataPath(string basePath) { - return System.IO.Path.Combine(basePath, "livetv", Id.ToString("N"), "metadata"); + return System.IO.Path.Combine(basePath, "livetv", Id.ToString("N", CultureInfo.InvariantCulture), "metadata"); } public override bool CanDelete() { return false; } - - [IgnoreDataMember] - public bool IsMovie { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is sports. - /// </summary> - /// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value> - [IgnoreDataMember] - public bool IsSports { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is series. - /// </summary> - /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value> - [IgnoreDataMember] - public bool IsSeries { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is news. - /// </summary> - /// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value> - [IgnoreDataMember] - public bool IsNews { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is kids. - /// </summary> - /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value> - [IgnoreDataMember] - public bool IsKids => Tags.Contains("Kids", StringComparer.OrdinalIgnoreCase); - - [IgnoreDataMember] - public bool IsRepeat { get; set; } - - /// <summary> - /// Gets or sets the episode title. - /// </summary> - /// <value>The episode title.</value> - [IgnoreDataMember] - public string EpisodeTitle { get; set; } } } diff --git a/MediaBrowser.Controller/LiveTv/LiveTvConflictException.cs b/MediaBrowser.Controller/LiveTv/LiveTvConflictException.cs index df8f91eec..881c42c73 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvConflictException.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvConflictException.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.LiveTv @@ -9,12 +11,11 @@ namespace MediaBrowser.Controller.LiveTv { public LiveTvConflictException() { - } + public LiveTvConflictException(string message) : base(message) { - } } } diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs index 8bde6a5da..111dc0d27 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs @@ -1,160 +1,126 @@ +#nullable disable + +#pragma warning disable CS1591, SA1306 + using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.LiveTv { public class LiveTvProgram : BaseItem, IHasLookupInfo<ItemLookupInfo>, IHasStartDate, IHasProgramAttributes { + private static string EmbyServiceName = "Emby"; + public LiveTvProgram() { IsVirtualItem = true; } - public override List<string> GetUserDataKeys() - { - var list = base.GetUserDataKeys(); - - if (!IsSeries) - { - var key = this.GetProviderId(MetadataProviders.Imdb); - if (!string.IsNullOrEmpty(key)) - { - list.Insert(0, key); - } - - key = this.GetProviderId(MetadataProviders.Tmdb); - if (!string.IsNullOrEmpty(key)) - { - list.Insert(0, key); - } - } - else if (!string.IsNullOrEmpty(EpisodeTitle)) - { - var name = GetClientTypeName(); - - list.Insert(0, name + "-" + Name + (EpisodeTitle ?? string.Empty)); - } - - return list; - } - - private static string EmbyServiceName = "Emby"; - public override double GetDefaultPrimaryImageAspectRatio() - { - var serviceName = ServiceName; - - if (string.Equals(serviceName, EmbyServiceName, StringComparison.OrdinalIgnoreCase) || string.Equals(serviceName, "Next Pvr", StringComparison.OrdinalIgnoreCase)) - { - return 2.0 / 3; - } - else - { - return 16.0 / 9; - } - } + public string SeriesName { get; set; } - [IgnoreDataMember] + [JsonIgnore] public override SourceType SourceType => SourceType.LiveTV; /// <summary> - /// The start date of the program, in UTC. + /// Gets or sets start date of the program, in UTC. /// </summary> - [IgnoreDataMember] + [JsonIgnore] public DateTime StartDate { get; set; } /// <summary> /// Gets or sets a value indicating whether this instance is repeat. /// </summary> /// <value><c>true</c> if this instance is repeat; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsRepeat { get; set; } /// <summary> /// Gets or sets the episode title. /// </summary> /// <value>The episode title.</value> - [IgnoreDataMember] + [JsonIgnore] public string EpisodeTitle { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string ShowId { get; set; } /// <summary> /// Gets or sets a value indicating whether this instance is movie. /// </summary> /// <value><c>true</c> if this instance is movie; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsMovie { get; set; } /// <summary> - /// Gets or sets a value indicating whether this instance is sports. + /// Gets a value indicating whether this instance is sports. /// </summary> /// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsSports => Tags.Contains("Sports", StringComparer.OrdinalIgnoreCase); /// <summary> /// Gets or sets a value indicating whether this instance is series. /// </summary> /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsSeries { get; set; } /// <summary> - /// Gets or sets a value indicating whether this instance is live. + /// Gets a value indicating whether this instance is live. /// </summary> /// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsLive => Tags.Contains("Live", StringComparer.OrdinalIgnoreCase); /// <summary> - /// Gets or sets a value indicating whether this instance is news. + /// Gets a value indicating whether this instance is news. /// </summary> /// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsNews => Tags.Contains("News", StringComparer.OrdinalIgnoreCase); /// <summary> - /// Gets or sets a value indicating whether this instance is kids. + /// Gets a value indicating whether this instance is kids. /// </summary> /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsKids => Tags.Contains("Kids", StringComparer.OrdinalIgnoreCase); /// <summary> - /// Gets or sets a value indicating whether this instance is premiere. + /// Gets a value indicating whether this instance is premiere. /// </summary> /// <value><c>true</c> if this instance is premiere; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsPremiere => Tags.Contains("Premiere", StringComparer.OrdinalIgnoreCase); /// <summary> - /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself + /// Gets the folder containing the item. + /// If the item is a folder, it returns the folder itself. /// </summary> /// <value>The containing folder path.</value> - [IgnoreDataMember] + [JsonIgnore] public override string ContainingFolderPath => Path; - //[IgnoreDataMember] - //public override string MediaType - //{ + // [JsonIgnore] + // public override string MediaType + // { // get // { // return ChannelType == ChannelType.TV ? Model.Entities.MediaType.Video : Model.Entities.MediaType.Audio; // } - //} + // } - [IgnoreDataMember] + [JsonIgnore] public bool IsAiring { get @@ -165,7 +131,7 @@ namespace MediaBrowser.Controller.LiveTv } } - [IgnoreDataMember] + [JsonIgnore] public bool HasAired { get @@ -176,6 +142,66 @@ namespace MediaBrowser.Controller.LiveTv } } + [JsonIgnore] + public override bool SupportsPeople + { + get + { + // Optimization + if (IsNews || IsSports) + { + return false; + } + + return base.SupportsPeople; + } + } + + [JsonIgnore] + public override bool SupportsAncestors => false; + + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + if (!IsSeries) + { + var key = this.GetProviderId(MetadataProvider.Imdb); + if (!string.IsNullOrEmpty(key)) + { + list.Insert(0, key); + } + + key = this.GetProviderId(MetadataProvider.Tmdb); + if (!string.IsNullOrEmpty(key)) + { + list.Insert(0, key); + } + } + else if (!string.IsNullOrEmpty(EpisodeTitle)) + { + var name = GetClientTypeName(); + + list.Insert(0, name + "-" + Name + (EpisodeTitle ?? string.Empty)); + } + + return list; + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + var serviceName = ServiceName; + + if (string.Equals(serviceName, EmbyServiceName, StringComparison.OrdinalIgnoreCase) || string.Equals(serviceName, "Next Pvr", StringComparison.OrdinalIgnoreCase)) + { + return 2.0 / 3; + } + else + { + return 16.0 / 9; + } + } + public override string GetClientTypeName() { return "Program"; @@ -188,7 +214,7 @@ namespace MediaBrowser.Controller.LiveTv protected override string GetInternalMetadataPath(string basePath) { - return System.IO.Path.Combine(basePath, "livetv", Id.ToString("N")); + return System.IO.Path.Combine(basePath, "livetv", Id.ToString("N", CultureInfo.InvariantCulture)); } public override bool CanDelete() @@ -196,24 +222,6 @@ namespace MediaBrowser.Controller.LiveTv return false; } - [IgnoreDataMember] - public override bool SupportsPeople - { - get - { - // Optimization - if (IsNews || IsSports) - { - return false; - } - - return base.SupportsPeople; - } - } - - [IgnoreDataMember] - public override bool SupportsAncestors => false; - private LiveTvOptions GetConfiguration() { return ConfigurationManager.GetConfiguration<LiveTvOptions>("livetv"); @@ -252,7 +260,7 @@ namespace MediaBrowser.Controller.LiveTv { var list = base.GetRelatedUrls(); - var imdbId = this.GetProviderId(MetadataProviders.Imdb); + var imdbId = this.GetProviderId(MetadataProvider.Imdb); if (!string.IsNullOrEmpty(imdbId)) { if (IsMovie) @@ -260,14 +268,12 @@ namespace MediaBrowser.Controller.LiveTv list.Add(new ExternalUrl { Name = "Trakt", - Url = string.Format("https://trakt.tv/movies/{0}", imdbId) + Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/movies/{0}", imdbId) }); } } return list; } - - public string SeriesName { get; set; } } } diff --git a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs b/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs index 67b2f0eb1..eb3babc18 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System.Collections.Generic; using MediaBrowser.Model.LiveTv; @@ -5,6 +9,12 @@ namespace MediaBrowser.Controller.LiveTv { public class LiveTvServiceStatusInfo { + public LiveTvServiceStatusInfo() + { + Tuners = new List<LiveTvTunerInfo>(); + IsVisible = true; + } + /// <summary> /// Gets or sets the status. /// </summary> @@ -34,16 +44,11 @@ namespace MediaBrowser.Controller.LiveTv /// </summary> /// <value>The tuners.</value> public List<LiveTvTunerInfo> Tuners { get; set; } + /// <summary> /// Gets or sets a value indicating whether this instance is visible. /// </summary> /// <value><c>true</c> if this instance is visible; otherwise, <c>false</c>.</value> public bool IsVisible { get; set; } - - public LiveTvServiceStatusInfo() - { - Tuners = new List<LiveTvTunerInfo>(); - IsVisible = true; - } } } diff --git a/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs b/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs index 2857f73f6..aa5eb59d1 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System.Collections.Generic; using MediaBrowser.Model.LiveTv; @@ -5,6 +9,11 @@ namespace MediaBrowser.Controller.LiveTv { public class LiveTvTunerInfo { + public LiveTvTunerInfo() + { + Clients = new List<string>(); + } + /// <summary> /// Gets or sets the type of the source. /// </summary> @@ -64,10 +73,5 @@ namespace MediaBrowser.Controller.LiveTv /// </summary> /// <value><c>true</c> if this instance can reset; otherwise, <c>false</c>.</value> public bool CanReset { get; set; } - - public LiveTvTunerInfo() - { - Clients = new List<string>(); - } } } diff --git a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs index 5d0f13192..3c3ac2471 100644 --- a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs +++ b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.LiveTv; @@ -6,8 +10,16 @@ namespace MediaBrowser.Controller.LiveTv { public class ProgramInfo { + public ProgramInfo() + { + Genres = new List<string>(); + + ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + /// <summary> - /// Id of the program. + /// Gets or sets the id of the program. /// </summary> public string Id { get; set; } @@ -18,7 +30,7 @@ namespace MediaBrowser.Controller.LiveTv public string ChannelId { get; set; } /// <summary> - /// Name of the program + /// Gets or sets the name of the program. /// </summary> public string Name { get; set; } @@ -33,6 +45,7 @@ namespace MediaBrowser.Controller.LiveTv /// </summary> /// <value>The overview.</value> public string Overview { get; set; } + /// <summary> /// Gets or sets the short overview. /// </summary> @@ -40,17 +53,17 @@ namespace MediaBrowser.Controller.LiveTv public string ShortOverview { get; set; } /// <summary> - /// The start date of the program, in UTC. + /// Gets or sets the start date of the program, in UTC. /// </summary> public DateTime StartDate { get; set; } /// <summary> - /// The end date of the program, in UTC. + /// Gets or sets the end date of the program, in UTC. /// </summary> public DateTime EndDate { get; set; } /// <summary> - /// Genre of the program. + /// Gets or sets the genre of the program. /// </summary> public List<string> Genres { get; set; } @@ -66,6 +79,9 @@ namespace MediaBrowser.Controller.LiveTv /// <value><c>true</c> if this instance is hd; otherwise, <c>false</c>.</value> public bool? IsHD { get; set; } + /// <summary> + /// Gets or sets a value indicating whether this instance is 3d. + /// </summary> public bool? Is3D { get; set; } /// <summary> @@ -95,13 +111,13 @@ namespace MediaBrowser.Controller.LiveTv public string EpisodeTitle { get; set; } /// <summary> - /// Supply the image path if it can be accessed directly from the file system + /// Gets or sets the image path if it can be accessed directly from the file system. /// </summary> /// <value>The image path.</value> public string ImagePath { get; set; } /// <summary> - /// Supply the image url if it can be downloaded + /// Gets or sets the image url if it can be downloaded. /// </summary> /// <value>The image URL.</value> public string ImageUrl { get; set; } @@ -167,31 +183,37 @@ namespace MediaBrowser.Controller.LiveTv /// </summary> /// <value>The production year.</value> public int? ProductionYear { get; set; } + /// <summary> /// Gets or sets the home page URL. /// </summary> /// <value>The home page URL.</value> public string HomePageUrl { get; set; } + /// <summary> /// Gets or sets the series identifier. /// </summary> /// <value>The series identifier.</value> public string SeriesId { get; set; } + /// <summary> /// Gets or sets the show identifier. /// </summary> /// <value>The show identifier.</value> public string ShowId { get; set; } + /// <summary> /// Gets or sets the season number. /// </summary> /// <value>The season number.</value> public int? SeasonNumber { get; set; } + /// <summary> /// Gets or sets the episode number. /// </summary> /// <value>The episode number.</value> public int? EpisodeNumber { get; set; } + /// <summary> /// Gets or sets the etag. /// </summary> @@ -199,14 +221,7 @@ namespace MediaBrowser.Controller.LiveTv public string Etag { get; set; } public Dictionary<string, string> ProviderIds { get; set; } - public Dictionary<string, string> SeriesProviderIds { get; set; } - - public ProgramInfo() - { - Genres = new List<string>(); - ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } + public Dictionary<string, string> SeriesProviderIds { get; set; } } } diff --git a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs index 432388d6b..1dcf7a58f 100644 --- a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs +++ b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.LiveTv; @@ -6,8 +10,13 @@ namespace MediaBrowser.Controller.LiveTv { public class RecordingInfo { + public RecordingInfo() + { + Genres = new List<string>(); + } + /// <summary> - /// Id of the recording. + /// Gets or sets the id of the recording. /// </summary> public string Id { get; set; } @@ -24,7 +33,7 @@ namespace MediaBrowser.Controller.LiveTv public string TimerId { get; set; } /// <summary> - /// ChannelId of the recording. + /// Gets or sets the channelId of the recording. /// </summary> public string ChannelId { get; set; } @@ -35,7 +44,7 @@ namespace MediaBrowser.Controller.LiveTv public ChannelType ChannelType { get; set; } /// <summary> - /// Name of the recording. + /// Gets or sets the name of the recording. /// </summary> public string Name { get; set; } @@ -58,12 +67,12 @@ namespace MediaBrowser.Controller.LiveTv public string Overview { get; set; } /// <summary> - /// The start date of the recording, in UTC. + /// Gets or sets the start date of the recording, in UTC. /// </summary> public DateTime StartDate { get; set; } /// <summary> - /// The end date of the recording, in UTC. + /// Gets or sets the end date of the recording, in UTC. /// </summary> public DateTime EndDate { get; set; } @@ -80,7 +89,7 @@ namespace MediaBrowser.Controller.LiveTv public RecordingStatus Status { get; set; } /// <summary> - /// Genre of the program. + /// Gets or sets the genre of the program. /// </summary> public List<string> Genres { get; set; } @@ -169,13 +178,13 @@ namespace MediaBrowser.Controller.LiveTv public float? CommunityRating { get; set; } /// <summary> - /// Supply the image path if it can be accessed directly from the file system + /// Gets or sets the image path if it can be accessed directly from the file system. /// </summary> /// <value>The image path.</value> public string ImagePath { get; set; } /// <summary> - /// Supply the image url if it can be downloaded + /// Gets or sets the image url if it can be downloaded. /// </summary> /// <value>The image URL.</value> public string ImageUrl { get; set; } @@ -185,6 +194,7 @@ namespace MediaBrowser.Controller.LiveTv /// </summary> /// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value> public bool? HasImage { get; set; } + /// <summary> /// Gets or sets the show identifier. /// </summary> @@ -196,10 +206,5 @@ namespace MediaBrowser.Controller.LiveTv /// </summary> /// <value>The date last updated.</value> public DateTime DateLastUpdated { get; set; } - - public RecordingInfo() - { - Genres = new List<string>(); - } } } diff --git a/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs b/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs index 99460a686..0b943c939 100644 --- a/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs +++ b/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.LiveTv; diff --git a/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs b/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs index 4fbd496c5..d6811fe14 100644 --- a/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs +++ b/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.LiveTv; @@ -6,13 +10,20 @@ namespace MediaBrowser.Controller.LiveTv { public class SeriesTimerInfo { + public SeriesTimerInfo() + { + Days = new List<DayOfWeek>(); + SkipEpisodesInLibrary = true; + KeepUntil = KeepUntil.UntilDeleted; + } + /// <summary> - /// Id of the recording. + /// Gets or sets the id of the recording. /// </summary> public string Id { get; set; } /// <summary> - /// ChannelId of the recording. + /// Gets or sets the channelId of the recording. /// </summary> public string ChannelId { get; set; } @@ -23,24 +34,27 @@ namespace MediaBrowser.Controller.LiveTv public string ProgramId { get; set; } /// <summary> - /// Name of the recording. + /// Gets or sets the name of the recording. /// </summary> public string Name { get; set; } + /// <summary> + /// Gets or sets the service name. + /// </summary> public string ServiceName { get; set; } /// <summary> - /// Description of the recording. + /// Gets or sets the description of the recording. /// </summary> public string Overview { get; set; } /// <summary> - /// The start date of the recording, in UTC. + /// Gets or sets the start date of the recording, in UTC. /// </summary> public DateTime StartDate { get; set; } /// <summary> - /// The end date of the recording, in UTC. + /// Gets or sets the end date of the recording, in UTC. /// </summary> public DateTime EndDate { get; set; } @@ -57,6 +71,7 @@ namespace MediaBrowser.Controller.LiveTv public bool RecordAnyChannel { get; set; } public int KeepUpTo { get; set; } + public KeepUntil KeepUntil { get; set; } public bool SkipEpisodesInLibrary { get; set; } @@ -108,12 +123,5 @@ namespace MediaBrowser.Controller.LiveTv /// </summary> /// <value>The series identifier.</value> public string SeriesId { get; set; } - - public SeriesTimerInfo() - { - Days = new List<DayOfWeek>(); - SkipEpisodesInLibrary = true; - KeepUntil = KeepUntil.UntilDeleted; - } } } diff --git a/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs index cfec39b4e..92eb0be9c 100644 --- a/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs +++ b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs @@ -1,10 +1,18 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.LiveTv { public class TimerEventInfo { - public string Id { get; set; } - public Guid ProgramId { get; set; } + public TimerEventInfo(string id) + { + Id = id; + } + + public string Id { get; } + + public Guid? ProgramId { get; set; } } } diff --git a/MediaBrowser.Controller/LiveTv/TimerInfo.cs b/MediaBrowser.Controller/LiveTv/TimerInfo.cs index 2fea6a8cb..1a2e8acb3 100644 --- a/MediaBrowser.Controller/LiveTv/TimerInfo.cs +++ b/MediaBrowser.Controller/LiveTv/TimerInfo.cs @@ -1,8 +1,12 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using MediaBrowser.Model.LiveTv; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.LiveTv { @@ -18,22 +22,23 @@ namespace MediaBrowser.Controller.LiveTv } public Dictionary<string, string> ProviderIds { get; set; } + public Dictionary<string, string> SeriesProviderIds { get; set; } + public string[] Tags { get; set; } /// <summary> - /// Id of the recording. + /// Gets or sets the id of the recording. /// </summary> public string Id { get; set; } /// <summary> /// Gets or sets the series timer identifier. /// </summary> - /// <value>The series timer identifier.</value> public string SeriesTimerId { get; set; } /// <summary> - /// ChannelId of the recording. + /// Gets or sets the channelId of the recording. /// </summary> public string ChannelId { get; set; } @@ -46,24 +51,24 @@ namespace MediaBrowser.Controller.LiveTv public string ShowId { get; set; } /// <summary> - /// Name of the recording. + /// Gets or sets the name of the recording. /// </summary> public string Name { get; set; } /// <summary> - /// Description of the recording. + /// Gets or sets the description of the recording. /// </summary> public string Overview { get; set; } public string SeriesId { get; set; } /// <summary> - /// The start date of the recording, in UTC. + /// Gets or sets the start date of the recording, in UTC. /// </summary> public DateTime StartDate { get; set; } /// <summary> - /// The end date of the recording, in UTC. + /// Gets or sets the end date of the recording, in UTC. /// </summary> public DateTime EndDate { get; set; } @@ -109,6 +114,7 @@ namespace MediaBrowser.Controller.LiveTv // Program properties public int? SeasonNumber { get; set; } + /// <summary> /// Gets or sets the episode number. /// </summary> @@ -126,13 +132,13 @@ namespace MediaBrowser.Controller.LiveTv public bool IsSeries { get; set; } /// <summary> - /// Gets or sets a value indicating whether this instance is live. + /// Gets a value indicating whether this instance is live. /// </summary> /// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsLive => Tags.Contains("Live", StringComparer.OrdinalIgnoreCase); - [IgnoreDataMember] + [JsonIgnore] public bool IsPremiere => Tags.Contains("Premiere", StringComparer.OrdinalIgnoreCase); public int? ProductionYear { get; set; } @@ -146,10 +152,15 @@ namespace MediaBrowser.Controller.LiveTv public bool IsRepeat { get; set; } public string HomePageUrl { get; set; } + public float? CommunityRating { get; set; } + public string OfficialRating { get; set; } + public string[] Genres { get; set; } + public string RecordingPath { get; set; } + public KeepUntil KeepUntil { get; set; } } } diff --git a/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs b/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs index cb02da635..1c1a4417d 100644 --- a/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs +++ b/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs @@ -1,10 +1,17 @@ +#nullable disable + +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.LiveTv { public class TunerChannelMapping { public string Name { get; set; } + public string ProviderChannelName { get; set; } + public string ProviderChannelId { get; set; } + public string Id { get; set; } } } diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 01893f1b5..d37880865 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -1,15 +1,30 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}</ProjectGuid> + </PropertyGroup> + <PropertyGroup> <Authors>Jellyfin Contributors</Authors> <PackageId>Jellyfin.Controller</PackageId> - <PackageLicenseUrl>https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt</PackageLicenseUrl> + <VersionPrefix>10.8.0</VersionPrefix> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> + <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> </PropertyGroup> <ItemGroup> - <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> - <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> + <PackageReference Include="Diacritics" Version="3.3.4" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0-rc.2*" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0-rc.2*" /> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> + <PackageReference Include="System.Threading.Tasks.Dataflow" Version="6.0.0-rc.2*" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="../Emby.Naming/Emby.Naming.csproj" /> + <ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" /> + <ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" /> </ItemGroup> <ItemGroup> @@ -17,8 +32,25 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>net6.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <PublishRepositoryUrl>true</PublishRepositoryUrl> + <EmbedUntrackedSources>true</EmbedUntrackedSources> + <IncludeSymbols>true</IncludeSymbols> + <SymbolPackageFormat>snupkg</SymbolPackageFormat> + </PropertyGroup> + + <PropertyGroup Condition=" '$(Stability)'=='Unstable'"> + <!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. --> + <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder> </PropertyGroup> + <!-- Code Analyzers--> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + </ItemGroup> + </Project> diff --git a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs new file mode 100644 index 000000000..dd6f468da --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs @@ -0,0 +1,204 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Dlna; + +namespace MediaBrowser.Controller.MediaEncoding +{ + public class BaseEncodingJobOptions + { + public BaseEncodingJobOptions() + { + EnableAutoStreamCopy = true; + AllowVideoStreamCopy = true; + AllowAudioStreamCopy = true; + Context = EncodingContext.Streaming; + StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + public Guid Id { get; set; } + + public string MediaSourceId { get; set; } + + public string DeviceId { get; set; } + + public string Container { get; set; } + + /// <summary> + /// Gets or sets the audio codec. + /// </summary> + /// <value>The audio codec.</value> + public string AudioCodec { get; set; } + + public bool EnableAutoStreamCopy { get; set; } + + public bool AllowVideoStreamCopy { get; set; } + + public bool AllowAudioStreamCopy { get; set; } + + public bool BreakOnNonKeyFrames { get; set; } + + /// <summary> + /// Gets or sets the audio sample rate. + /// </summary> + /// <value>The audio sample rate.</value> + public int? AudioSampleRate { get; set; } + + public int? MaxAudioBitDepth { get; set; } + + /// <summary> + /// Gets or sets the audio bit rate. + /// </summary> + /// <value>The audio bit rate.</value> + public int? AudioBitRate { get; set; } + + /// <summary> + /// Gets or sets the audio channels. + /// </summary> + /// <value>The audio channels.</value> + public int? AudioChannels { get; set; } + + public int? MaxAudioChannels { get; set; } + + public bool Static { get; set; } + + /// <summary> + /// Gets or sets the profile. + /// </summary> + /// <value>The profile.</value> + public string Profile { get; set; } + + /// <summary> + /// Gets or sets the level. + /// </summary> + /// <value>The level.</value> + public string Level { get; set; } + + /// <summary> + /// Gets or sets the framerate. + /// </summary> + /// <value>The framerate.</value> + public float? Framerate { get; set; } + + public float? MaxFramerate { get; set; } + + public bool CopyTimestamps { get; set; } + + /// <summary> + /// Gets or sets the start time ticks. + /// </summary> + /// <value>The start time ticks.</value> + public long? StartTimeTicks { get; set; } + + /// <summary> + /// Gets or sets the width. + /// </summary> + /// <value>The width.</value> + public int? Width { get; set; } + + /// <summary> + /// Gets or sets the height. + /// </summary> + /// <value>The height.</value> + public int? Height { get; set; } + + /// <summary> + /// Gets or sets the width of the max. + /// </summary> + /// <value>The width of the max.</value> + public int? MaxWidth { get; set; } + + /// <summary> + /// Gets or sets the height of the max. + /// </summary> + /// <value>The height of the max.</value> + public int? MaxHeight { get; set; } + + /// <summary> + /// Gets or sets the video bit rate. + /// </summary> + /// <value>The video bit rate.</value> + public int? VideoBitRate { get; set; } + + /// <summary> + /// Gets or sets the index of the subtitle stream. + /// </summary> + /// <value>The index of the subtitle stream.</value> + public int? SubtitleStreamIndex { get; set; } + + public SubtitleDeliveryMethod SubtitleMethod { get; set; } + + public int? MaxRefFrames { get; set; } + + public int? MaxVideoBitDepth { get; set; } + + public bool RequireAvc { get; set; } + + public bool DeInterlace { get; set; } + + public bool RequireNonAnamorphic { get; set; } + + public int? TranscodingMaxAudioChannels { get; set; } + + public int? CpuCoreLimit { get; set; } + + public string LiveStreamId { get; set; } + + public bool EnableMpegtsM2TsMode { get; set; } + + /// <summary> + /// Gets or sets the video codec. + /// </summary> + /// <value>The video codec.</value> + public string VideoCodec { get; set; } + + public string SubtitleCodec { get; set; } + + public string TranscodeReasons { get; set; } + + /// <summary> + /// Gets or sets the index of the audio stream. + /// </summary> + /// <value>The index of the audio stream.</value> + public int? AudioStreamIndex { get; set; } + + /// <summary> + /// Gets or sets the index of the video stream. + /// </summary> + /// <value>The index of the video stream.</value> + public int? VideoStreamIndex { get; set; } + + public EncodingContext Context { get; set; } + + public Dictionary<string, string> StreamOptions { get; set; } + + public string GetOption(string qualifier, string name) + { + var value = GetOption(qualifier + "-" + name); + + if (string.IsNullOrEmpty(value)) + { + value = GetOption(name); + } + + return value; + } + + public string GetOption(string name) + { + if (StreamOptions.TryGetValue(name, out var value)) + { + return value; + } + + return null; + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index f5f147db1..5715194b8 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1,136 +1,225 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Text; +using System.Text.RegularExpressions; using System.Threading; -using MediaBrowser.Controller.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; -using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Controller.MediaEncoding { public class EncodingHelper { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private readonly IMediaEncoder _mediaEncoder; - private readonly IFileSystem _fileSystem; private readonly ISubtitleEncoder _subtitleEncoder; - // private readonly IApplicationPaths _appPaths; - // private readonly IAssemblyInfo _assemblyInfo; - public EncodingHelper(IMediaEncoder mediaEncoder, IFileSystem fileSystem, ISubtitleEncoder subtitleEncoder) + private static readonly string[] _videoProfiles = new[] + { + "ConstrainedBaseline", + "Baseline", + "Extended", + "Main", + "High", + "ProgressiveHigh", + "ConstrainedHigh" + }; + + private static readonly Version _minVersionForCudaOverlay = new Version(4, 4); + + public EncodingHelper( + IMediaEncoder mediaEncoder, + ISubtitleEncoder subtitleEncoder) { _mediaEncoder = mediaEncoder; - _fileSystem = fileSystem; _subtitleEncoder = subtitleEncoder; } public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions) - { - var defaultEncoder = "libx264"; + => GetH264OrH265Encoder("libx264", "h264", state, encodingOptions); + + public string GetH265Encoder(EncodingJobInfo state, EncodingOptions encodingOptions) + => GetH264OrH265Encoder("libx265", "hevc", state, encodingOptions); + private string GetH264OrH265Encoder(string defaultEncoder, string hwEncoder, EncodingJobInfo state, EncodingOptions encodingOptions) + { // Only use alternative encoders for video files. // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully - // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this. + // Since transcoding of folder rips is experimental anyway, it's not worth adding additional variables such as this. if (state.VideoType == VideoType.VideoFile) { var hwType = encodingOptions.HardwareAccelerationType; var codecMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { - {"qsv", "h264_qsv"}, - {"h264_qsv", "h264_qsv"}, - {"nvenc", "h264_nvenc"}, - {"amf", "h264_amf"}, - {"omx", "h264_omx"}, - {"h264_v4l2m2m", "h264_v4l2m2m"}, - {"mediacodec", "h264_mediacodec"}, - {"vaapi", "h264_vaapi"} + { "qsv", hwEncoder + "_qsv" }, + { hwEncoder + "_qsv", hwEncoder + "_qsv" }, + { "nvenc", hwEncoder + "_nvenc" }, + { "amf", hwEncoder + "_amf" }, + { "omx", hwEncoder + "_omx" }, + { hwEncoder + "_v4l2m2m", hwEncoder + "_v4l2m2m" }, + { "mediacodec", hwEncoder + "_mediacodec" }, + { "vaapi", hwEncoder + "_vaapi" }, + { "videotoolbox", hwEncoder + "_videotoolbox" } }; if (!string.IsNullOrEmpty(hwType) - && encodingOptions.EnableHardwareEncoding && codecMap.ContainsKey(hwType)) + && encodingOptions.EnableHardwareEncoding + && codecMap.ContainsKey(hwType)) { - if (CheckVaapi(state, hwType, encodingOptions)) - { - var preferredEncoder = codecMap[hwType]; + var preferredEncoder = codecMap[hwType]; - if (_mediaEncoder.SupportsEncoder(preferredEncoder)) - { - return preferredEncoder; - } + if (_mediaEncoder.SupportsEncoder(preferredEncoder)) + { + return preferredEncoder; } } - } - // Avoid performing a second attempt when the first one - // hasn't tried hardware encoding anyway. - encodingOptions.EnableHardwareEncoding = false; return defaultEncoder; } - private bool CheckVaapi(EncodingJobInfo state, string hwType, EncodingOptions encodingOptions) + private bool IsVaapiSupported(EncodingJobInfo state) { - if (!string.Equals(hwType, "vaapi", StringComparison.OrdinalIgnoreCase)) + var videoStream = state.VideoStream; + + // vaapi will throw an error with this input + // [vaapi @ 0x7faed8000960] No VAAPI support for codec mpeg4 profile -99. + if (string.Equals(videoStream?.Codec, "mpeg4", StringComparison.OrdinalIgnoreCase)) { - // No vaapi requested, return OK. - return true; + return false; } - if (string.IsNullOrEmpty(encodingOptions.VaapiDevice)) + return _mediaEncoder.SupportsHwaccel("vaapi"); + } + + private bool IsCudaSupported() + { + return _mediaEncoder.SupportsHwaccel("cuda") + && _mediaEncoder.SupportsFilter("scale_cuda") + && _mediaEncoder.SupportsFilter("yadif_cuda") + && _mediaEncoder.SupportsFilter("hwupload_cuda"); + } + + private bool IsOpenclTonemappingSupported(EncodingJobInfo state, EncodingOptions options) + { + var videoStream = state.VideoStream; + if (videoStream == null) { - // No device specified, return OK. - return true; + return false; } - return IsVaapiSupported(state); + return options.EnableTonemapping + && (string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.ColorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase)) + && IsColorDepth10(state) + && _mediaEncoder.SupportsHwaccel("opencl") + && _mediaEncoder.SupportsFilter("tonemap_opencl"); } - private bool IsVaapiSupported(EncodingJobInfo state) + private bool IsCudaTonemappingSupported(EncodingJobInfo state, EncodingOptions options) { var videoStream = state.VideoStream; + if (videoStream == null) + { + return false; + } - if (videoStream != null) + return options.EnableTonemapping + && (string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.ColorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase)) + && IsColorDepth10(state) + && _mediaEncoder.SupportsHwaccel("cuda") + && _mediaEncoder.SupportsFilterWithOption(FilterOptionType.TonemapCudaName); + } + + private bool IsVppTonemappingSupported(EncodingJobInfo state, EncodingOptions options) + { + var videoStream = state.VideoStream; + if (videoStream == null) { - // vaapi will throw an error with this input - // [vaapi @ 0x7faed8000960] No VAAPI support for codec mpeg4 profile -99. - if (string.Equals(videoStream.Codec, "mpeg4", StringComparison.OrdinalIgnoreCase)) - { - return false; - } + // Remote stream doesn't have media info, disable vpp tonemapping. + return false; } - return true; + + var codec = videoStream.Codec; + if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) + { + // Limited to HEVC for now since the filter doesn't accept master data from VP9. + return options.EnableVppTonemapping + && string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase) + && IsColorDepth10(state) + && string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) + && _mediaEncoder.SupportsHwaccel("vaapi") + && _mediaEncoder.SupportsFilter("tonemap_vaapi"); + } + + // Hybrid VPP tonemapping for QSV with VAAPI + if (OperatingSystem.IsLinux() && string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) + { + // Limited to HEVC for now since the filter doesn't accept master data from VP9. + return options.EnableVppTonemapping + && string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase) + && IsColorDepth10(state) + && string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) + && _mediaEncoder.SupportsHwaccel("vaapi") + && _mediaEncoder.SupportsFilter("tonemap_vaapi") + && _mediaEncoder.SupportsHwaccel("qsv"); + } + + // Native VPP tonemapping may come to QSV in the future. + return false; } /// <summary> - /// Gets the name of the output video codec + /// Gets the name of the output video codec. /// </summary> + /// <param name="state">Encording state.</param> + /// <param name="encodingOptions">Encoding options.</param> + /// <returns>Encoder string.</returns> public string GetVideoEncoder(EncodingJobInfo state, EncodingOptions encodingOptions) { var codec = state.OutputVideoCodec; if (!string.IsNullOrEmpty(codec)) { + if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) + { + return GetH265Encoder(state, encodingOptions); + } + if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase)) { return GetH264Encoder(state, encodingOptions); } - if (string.Equals(codec, "vpx", StringComparison.OrdinalIgnoreCase)) + + if (string.Equals(codec, "vp8", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "vpx", StringComparison.OrdinalIgnoreCase)) { return "libvpx"; } + + if (string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase)) + { + return "libvpx-vp9"; + } + if (string.Equals(codec, "wmv", StringComparison.OrdinalIgnoreCase)) { return "wmv2"; } + if (string.Equals(codec, "theora", StringComparison.OrdinalIgnoreCase)) { return "libtheora"; @@ -149,11 +238,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// <returns>System.String.</returns> public string GetUserAgentParam(EncodingJobInfo state) { - string useragent = null; - - state.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent); - - if (!string.IsNullOrEmpty(useragent)) + if (state.RemoteHttpHeaders.TryGetValue("User-Agent", out string useragent)) { return "-user_agent \"" + useragent + "\""; } @@ -161,7 +246,7 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Empty; } - public string GetInputFormat(string container) + public static string GetInputFormat(string container) { if (string.IsNullOrEmpty(container)) { @@ -180,56 +265,68 @@ namespace MediaBrowser.Controller.MediaEncoding { return null; } + if (string.Equals(container, "wmv", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(container, "mts", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(container, "vob", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(container, "mpg", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(container, "mpeg", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(container, "rec", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(container, "dvr-ms", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(container, "ogm", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(container, "divx", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(container, "tp", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(container, "rmvb", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(container, "rtp", StringComparison.OrdinalIgnoreCase)) { return null; } - // Seeing reported failures here, not sure yet if this is related to specfying input format + // Seeing reported failures here, not sure yet if this is related to specifying input format if (string.Equals(container, "m4v", StringComparison.OrdinalIgnoreCase)) { return null; @@ -241,9 +338,20 @@ namespace MediaBrowser.Controller.MediaEncoding return null; } + // ISO files don't have an ffmpeg format + if (string.Equals(container, "iso", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + return container; } + /// <summary> + /// Gets decoder from a codec. + /// </summary> + /// <param name="codec">Codec to use.</param> + /// <returns>Decoder string.</returns> public string GetDecoderFromCodec(string codec) { // For these need to find out the ffmpeg names @@ -251,10 +359,12 @@ namespace MediaBrowser.Controller.MediaEncoding { return null; } + if (string.Equals(codec, "aac_latm", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(codec, "eac3", StringComparison.OrdinalIgnoreCase)) { return null; @@ -269,8 +379,10 @@ namespace MediaBrowser.Controller.MediaEncoding } /// <summary> - /// Infers the audio codec based on the url + /// Infers the audio codec based on the url. /// </summary> + /// <param name="container">Container to use.</param> + /// <returns>Codec string.</returns> public string InferAudioCodec(string container) { var ext = "." + (container ?? string.Empty); @@ -279,30 +391,37 @@ namespace MediaBrowser.Controller.MediaEncoding { return "mp3"; } + if (string.Equals(ext, ".aac", StringComparison.OrdinalIgnoreCase)) { return "aac"; } + if (string.Equals(ext, ".wma", StringComparison.OrdinalIgnoreCase)) { return "wma"; } + if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase)) { return "vorbis"; } + if (string.Equals(ext, ".oga", StringComparison.OrdinalIgnoreCase)) { return "vorbis"; } + if (string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase)) { return "vorbis"; } + if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase)) { return "vorbis"; } + if (string.Equals(ext, ".webma", StringComparison.OrdinalIgnoreCase)) { return "vorbis"; @@ -324,14 +443,18 @@ namespace MediaBrowser.Controller.MediaEncoding { return "wmv"; } + if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase)) { - return "vpx"; + // TODO: this may not always mean VP8, as the codec ages + return "vp8"; } + if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase)) { return "theora"; } + if (string.Equals(ext, ".m3u8", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ts", StringComparison.OrdinalIgnoreCase)) { return "h264"; @@ -342,37 +465,16 @@ namespace MediaBrowser.Controller.MediaEncoding public int GetVideoProfileScore(string profile) { - var list = new[] - { - "ConstrainedBaseline", - "Baseline", - "Extended", - "Main", - "High", - "ProgressiveHigh", - "ConstrainedHigh" - }; - // strip spaces because they may be stripped out on the query string - return Array.FindIndex(list, t => string.Equals(t, profile.Replace(" ", ""), StringComparison.OrdinalIgnoreCase)); + profile = profile.Replace(" ", string.Empty, StringComparison.Ordinal); + return Array.FindIndex(_videoProfiles, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase)); } public string GetInputPathArgument(EncodingJobInfo state) { - var protocol = state.InputProtocol; var mediaPath = state.MediaPath ?? string.Empty; - var inputPath = new[] { mediaPath }; - - if (state.IsInputVideo) - { - if (!(state.VideoType == VideoType.Iso && state.IsoMount == null)) - { - inputPath = MediaEncoderHelpers.GetInputArgument(_fileSystem, mediaPath, state.InputProtocol, state.IsoMount, state.PlayableStreamFileNames); - } - } - - return _mediaEncoder.GetInputArgument(inputPath, protocol); + return _mediaEncoder.GetInputArgument(mediaPath, state.MediaSource); } /// <summary> @@ -386,81 +488,215 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase)) { - return "aac -strict experimental"; + // Use libfdk_aac for better audio quality if using custom build of FFmpeg which has fdk_aac support + if (_mediaEncoder.SupportsEncoder("libfdk_aac")) + { + return "libfdk_aac"; + } + + return "aac"; } + if (string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase)) { return "libmp3lame"; } + if (string.Equals(codec, "vorbis", StringComparison.OrdinalIgnoreCase)) { return "libvorbis"; } + if (string.Equals(codec, "wma", StringComparison.OrdinalIgnoreCase)) { return "wmav2"; } + if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase)) { return "libopus"; } + if (string.Equals(codec, "flac", StringComparison.OrdinalIgnoreCase)) + { + // flac is experimental in mp4 muxer + return "flac -strict -2"; + } + return codec.ToLowerInvariant(); } /// <summary> /// Gets the input argument. /// </summary> - public string GetInputArgument(EncodingJobInfo state, EncodingOptions encodingOptions) + /// <param name="state">Encoding state.</param> + /// <param name="options">Encoding options.</param> + /// <returns>Input arguments.</returns> + public string GetInputArgument(EncodingJobInfo state, EncodingOptions options) { - var request = state.BaseRequest; + var arg = new StringBuilder(); + var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options) ?? string.Empty; + var outputVideoCodec = GetVideoEncoder(state, options) ?? string.Empty; + var isWindows = OperatingSystem.IsWindows(); + var isLinux = OperatingSystem.IsLinux(); + var isMacOS = OperatingSystem.IsMacOS(); +#pragma warning disable CA1508 // Defaults to string.Empty + var isSwDecoder = string.IsNullOrEmpty(videoDecoder); +#pragma warning restore CA1508 + var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1; + var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; + var isVaapiEncoder = outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; + var isQsvDecoder = videoDecoder.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1; + var isQsvEncoder = outputVideoCodec.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1; + var isNvdecDecoder = videoDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase); + var isCuvidHevcDecoder = videoDecoder.Contains("hevc_cuvid", StringComparison.OrdinalIgnoreCase); + var isCuvidVp9Decoder = videoDecoder.Contains("vp9_cuvid", StringComparison.OrdinalIgnoreCase); + var isOpenclTonemappingSupported = IsOpenclTonemappingSupported(state, options); + var isVppTonemappingSupported = IsVppTonemappingSupported(state, options); + var isCudaTonemappingSupported = IsCudaTonemappingSupported(state, options); + + if (!IsCopyCodec(outputVideoCodec)) + { + if (state.IsVideoRequest + && _mediaEncoder.SupportsHwaccel("vaapi") + && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) + { + if (isVaapiDecoder) + { + if (isOpenclTonemappingSupported && !isVppTonemappingSupported) + { + arg.Append("-init_hw_device vaapi=va:") + .Append(options.VaapiDevice) + .Append(" -init_hw_device opencl=ocl@va ") + .Append("-hwaccel_device va ") + .Append("-hwaccel_output_format vaapi ") + .Append("-filter_hw_device ocl "); + } + else + { + arg.Append("-hwaccel_output_format vaapi ") + .Append("-vaapi_device ") + .Append(options.VaapiDevice) + .Append(' '); + } + } + else if (!isVaapiDecoder && isVaapiEncoder) + { + arg.Append("-vaapi_device ") + .Append(options.VaapiDevice) + .Append(' '); + } - var arg = string.Format("-i {0}", GetInputPathArgument(state)); + arg.Append("-autorotate 0 "); + } - if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) - { - if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream) + if (state.IsVideoRequest + && string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) { - if (state.VideoStream != null && state.VideoStream.Width.HasValue) + var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + + if (isQsvEncoder) { - // This is hacky but not sure how to get the exact subtitle resolution - int height = Convert.ToInt32((double)state.VideoStream.Width.Value / 16.0 * 9.0); + if (isQsvDecoder) + { + if (isLinux) + { + if (hasGraphicalSubs) + { + arg.Append("-init_hw_device qsv=hw -filter_hw_device hw "); + } + else + { + arg.Append("-hwaccel qsv "); + } + } - arg += string.Format(" -canvas_size {0}:{1}", state.VideoStream.Width.Value.ToString(CultureInfo.InvariantCulture), height.ToString(CultureInfo.InvariantCulture)); - } + if (isWindows) + { + arg.Append("-hwaccel qsv "); + } + } - var subtitlePath = state.SubtitleStream.Path; + // While using SW decoder + else if (isSwDecoder) + { + arg.Append("-init_hw_device qsv=hw -filter_hw_device hw "); + } - if (string.Equals(Path.GetExtension(subtitlePath), ".sub", StringComparison.OrdinalIgnoreCase)) - { - var idxFile = Path.ChangeExtension(subtitlePath, ".idx"); - if (File.Exists(idxFile)) + // Hybrid VPP tonemapping with VAAPI + else if (isVaapiDecoder && isVppTonemappingSupported) { - subtitlePath = idxFile; + arg.Append("-init_hw_device vaapi=va:") + .Append(options.VaapiDevice) + .Append(" -init_hw_device qsv@va ") + .Append("-hwaccel_output_format vaapi "); } + + arg.Append("-autorotate 0 "); } + } - arg += " -i \"" + subtitlePath + "\""; + if (state.IsVideoRequest + && string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) + && isNvdecDecoder) + { + // Fix for 'No decoder surfaces left' error. https://trac.ffmpeg.org/ticket/7562 + arg.Append("-hwaccel_output_format cuda -extra_hw_frames 3 -autorotate 0 "); } - } - if (state.IsVideoRequest) - { - if (GetVideoEncoder(state, encodingOptions).IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1) + if (state.IsVideoRequest + && string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) + && (isNvdecDecoder || isCuvidHevcDecoder || isCuvidVp9Decoder || isSwDecoder)) { - var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; - var hwOutputFormat = "vaapi"; + if (!isCudaTonemappingSupported && isOpenclTonemappingSupported) + { + arg.Append("-init_hw_device opencl=ocl:") + .Append(options.OpenclDevice) + .Append(" -filter_hw_device ocl "); + } + } - if (hasGraphicalSubs) + if (state.IsVideoRequest + && string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) + && (isD3d11vaDecoder || isSwDecoder)) + { + if (isOpenclTonemappingSupported) { - hwOutputFormat = "yuv420p"; + arg.Append("-init_hw_device opencl=ocl:") + .Append(options.OpenclDevice) + .Append(" -filter_hw_device ocl "); } + } - arg = "-hwaccel vaapi -hwaccel_output_format " + hwOutputFormat + " -vaapi_device " + encodingOptions.VaapiDevice + " " + arg; + if (state.IsVideoRequest + && string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) + { + arg.Append("-hwaccel videotoolbox "); } } - return arg.Trim(); + arg.Append("-i ") + .Append(GetInputPathArgument(state)); + + if (state.SubtitleStream != null + && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode + && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream) + { + var subtitlePath = state.SubtitleStream.Path; + + if (string.Equals(Path.GetExtension(subtitlePath), ".sub", StringComparison.OrdinalIgnoreCase)) + { + var idxFile = Path.ChangeExtension(subtitlePath, ".idx"); + if (File.Exists(idxFile)) + { + subtitlePath = idxFile; + } + } + + arg.Append(" -i \"").Append(subtitlePath).Append('\"'); + } + + return arg.ToString(); } /// <summary> @@ -468,57 +704,138 @@ namespace MediaBrowser.Controller.MediaEncoding /// </summary> /// <param name="stream">The stream.</param> /// <returns><c>true</c> if the specified stream is H264; otherwise, <c>false</c>.</returns> - public bool IsH264(MediaStream stream) + public static bool IsH264(MediaStream stream) { var codec = stream.Codec ?? string.Empty; - return codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1 || - codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1; + return codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1 + || codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1; } - public string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec) + public static bool IsH265(MediaStream stream) { - var bitrate = state.OutputVideoBitrate; + var codec = stream.Codec ?? string.Empty; - if (bitrate.HasValue) + return codec.IndexOf("265", StringComparison.OrdinalIgnoreCase) != -1 + || codec.IndexOf("hevc", StringComparison.OrdinalIgnoreCase) != -1; + } + + public static bool IsAAC(MediaStream stream) + { + var codec = stream.Codec ?? string.Empty; + + return codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1; + } + + public static string GetBitStreamArgs(MediaStream stream) + { + // 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)) { - if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase)) - { - // With vpx when crf is used, b:v becomes a max rate - // https://trac.ffmpeg.org/wiki/vpxEncodingGuide. - return string.Format(" -maxrate:v {0} -bufsize:v {1} -b:v {0}", bitrate.Value.ToString(_usCulture), (bitrate.Value * 2).ToString(_usCulture)); - } + return "-bsf:v h264_mp4toannexb"; + } + else if (IsH265(stream)) + { + return "-bsf:v hevc_mp4toannexb"; + } + else if (IsAAC(stream)) + { + // Convert adts header(mpegts) to asc header(mp4). + return "-bsf:a aac_adtstoasc"; + } + else + { + return null; + } + } - if (string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase)) - { - return string.Format(" -b:v {0}", bitrate.Value.ToString(_usCulture)); - } + public static string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer) + { + var bitStreamArgs = string.Empty; + var segmentFormat = GetSegmentFileExtension(segmentContainer).TrimStart('.'); - if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase)) - { - // h264 - return string.Format(" -maxrate {0} -bufsize {1}", - bitrate.Value.ToString(_usCulture), - (bitrate.Value * 2).ToString(_usCulture)); - } + // Apply aac_adtstoasc bitstream filter when media source is in mpegts. + if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase) + && (string.Equals(mediaSourceContainer, "mpegts", StringComparison.OrdinalIgnoreCase) + || string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase))) + { + bitStreamArgs = GetBitStreamArgs(state.AudioStream); + bitStreamArgs = string.IsNullOrEmpty(bitStreamArgs) ? string.Empty : " " + bitStreamArgs; + } - // h264 - return string.Format(" -b:v {0} -maxrate {0} -bufsize {1}", - bitrate.Value.ToString(_usCulture), - (bitrate.Value * 2).ToString(_usCulture)); + return bitStreamArgs; + } + + public static string GetSegmentFileExtension(string segmentContainer) + { + if (!string.IsNullOrWhiteSpace(segmentContainer)) + { + return "." + segmentContainer; } - return string.Empty; + return ".ts"; + } + + public string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec) + { + if (state.OutputVideoBitrate == null) + { + return string.Empty; + } + + int bitrate = state.OutputVideoBitrate.Value; + + // Currently use the same buffer size for all encoders + int bufsize = bitrate * 2; + + if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoCodec, "libvpx-vp9", StringComparison.OrdinalIgnoreCase)) + { + // When crf is used with vpx, b:v becomes a max rate + // https://trac.ffmpeg.org/wiki/Encode/VP8 + // https://trac.ffmpeg.org/wiki/Encode/VP9 + return FormattableString.Invariant($" -maxrate:v {bitrate} -bufsize:v {bufsize} -b:v {bitrate}"); + } + + if (string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase)) + { + return FormattableString.Invariant($" -b:v {bitrate}"); + } + + if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoCodec, "libx265", StringComparison.OrdinalIgnoreCase)) + { + return FormattableString.Invariant($" -maxrate {bitrate} -bufsize {bufsize}"); + } + + return FormattableString.Invariant($" -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}"); } - public string NormalizeTranscodingLevel(string videoCodec, string level) + public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level) { - // Clients may direct play higher than level 41, but there's no reason to transcode higher - if (double.TryParse(level, NumberStyles.Any, _usCulture, out double requestLevel) - && string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) - && requestLevel > 41) + if (double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out double requestLevel)) { - return "41"; + if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)) + { + // Transcode to level 5.0 and lower for maximum compatibility. + // Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it. + // https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels + // MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880. + if (requestLevel >= 150) + { + return "150"; + } + } + else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase)) + { + // Clients may direct play higher than level 41, but there's no reason to transcode higher. + if (requestLevel >= 41) + { + return "41"; + } + } } return level; @@ -536,7 +853,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hls always copies timestamps var setPtsParam = state.CopyTimestamps || state.TranscodingType != TranscodingJobType.Progressive ? string.Empty - : string.Format(",setpts=PTS -{0}/TB", seconds.ToString(_usCulture)); + : string.Format(CultureInfo.InvariantCulture, ",setpts=PTS -{0}/TB", seconds); // TODO // var fallbackFontPath = Path.Combine(_appPaths.ProgramDataPath, "fonts", "DroidSansFallback.ttf"); @@ -547,14 +864,14 @@ namespace MediaBrowser.Controller.MediaEncoding // _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(fallbackFontPath)); // using (var stream = _assemblyInfo.GetManifestResourceStream(GetType(), GetType().Namespace + ".DroidSansFallback.ttf")) // { - // using (var fileStream = _fileSystem.GetFileStream(fallbackFontPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) + // using (var fileStream = new FileStream(fallbackFontPath, FileMode.Create, FileAccess.Write, FileShare.Read)) // { // stream.CopyTo(fileStream); // } // } // } - // fallbackFontParam = string.Format(":force_style='FontName=Droid Sans Fallback':fontsdir='{0}'", _mediaEncoder.EscapeSubtitleFilterPath(_fileSystem.GetDirectoryName(fallbackFontPath))); + // fallbackFontParam = string.Format(CultureInfo.InvariantCulture, ":force_style='FontName=Droid Sans Fallback':fontsdir='{0}'", _mediaEncoder.EscapeSubtitleFilterPath(_fileSystem.GetDirectoryName(fallbackFontPath))); if (state.SubtitleStream.IsExternal) { @@ -564,7 +881,11 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(state.SubtitleStream.Language)) { - var charenc = _subtitleEncoder.GetSubtitleFileCharacterSet(subtitlePath, state.SubtitleStream.Language, state.MediaSource.Protocol, CancellationToken.None).Result; + var charenc = _subtitleEncoder.GetSubtitleFileCharacterSet( + subtitlePath, + state.SubtitleStream.Language, + state.MediaSource.Protocol, + CancellationToken.None).GetAwaiter().GetResult(); if (!string.IsNullOrEmpty(charenc)) { @@ -573,7 +894,9 @@ namespace MediaBrowser.Controller.MediaEncoding } // TODO: Perhaps also use original_size=1920x800 ?? - return string.Format("subtitles=filename='{0}'{1}{2}{3}", + return string.Format( + CultureInfo.InvariantCulture, + "subtitles=filename='{0}'{1}{2}", _mediaEncoder.EscapeSubtitleFilterPath(subtitlePath), charsetParam, // fallbackFontParam, @@ -582,9 +905,11 @@ namespace MediaBrowser.Controller.MediaEncoding var mediaPath = state.MediaPath ?? string.Empty; - return string.Format("subtitles='{0}:si={1}'{2}", + return string.Format( + CultureInfo.InvariantCulture, + "subtitles='{0}:si={1}'{2}", _mediaEncoder.EscapeSubtitleFilterPath(mediaPath), - state.InternalSubtitleStreamOffset.ToString(_usCulture), + state.InternalSubtitleStreamOffset.ToString(CultureInfo.InvariantCulture), // fallbackFontParam, setPtsParam); } @@ -613,79 +938,199 @@ namespace MediaBrowser.Controller.MediaEncoding return null; } + public string GetHlsVideoKeyFrameArguments( + EncodingJobInfo state, + string codec, + int segmentLength, + bool isEventPlaylist, + int? startNumber) + { + var args = string.Empty; + var gopArg = string.Empty; + var keyFrameArg = string.Empty; + if (isEventPlaylist) + { + keyFrameArg = string.Format( + CultureInfo.InvariantCulture, + " -force_key_frames:0 \"expr:gte(t,n_forced*{0})\"", + segmentLength); + } + else if (startNumber.HasValue) + { + keyFrameArg = string.Format( + CultureInfo.InvariantCulture, + " -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"", + startNumber.Value * segmentLength, + segmentLength); + } + + var framerate = state.VideoStream?.RealFrameRate; + if (framerate.HasValue) + { + // This is to make sure keyframe interval is limited to our segment, + // as forcing keyframes is not enough. + // Example: we encoded half of desired length, then codec detected + // scene cut and inserted a keyframe; next forced keyframe would + // be created outside of segment, which breaks seeking. + // -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe. + gopArg = string.Format( + CultureInfo.InvariantCulture, + " -g:v:0 {0} -keyint_min:v:0 {0} -sc_threshold:v:0 0", + Math.Ceiling(segmentLength * framerate.Value)); + } + + // Unable to force key frames using these encoders, set key frames by GOP. + if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc_amf", StringComparison.OrdinalIgnoreCase)) + { + args += gopArg; + } + else if (string.Equals(codec, "libx264", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)) + { + args += " " + keyFrameArg; + } + else + { + args += " " + keyFrameArg + gopArg; + } + + return args; + } + /// <summary> - /// Gets the video bitrate to specify on the command line + /// Gets the video bitrate to specify on the command line. /// </summary> - public string GetVideoQualityParam(EncodingJobInfo state, string videoEncoder, EncodingOptions encodingOptions, string defaultH264Preset) + /// <param name="state">Encoding state.</param> + /// <param name="videoEncoder">Video encoder to use.</param> + /// <param name="encodingOptions">Encoding options.</param> + /// <param name="defaultPreset">Default present to use for encoding.</param> + /// <returns>Video bitrate.</returns> + public string GetVideoQualityParam(EncodingJobInfo state, string videoEncoder, EncodingOptions encodingOptions, string defaultPreset) { var param = string.Empty; + if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) + { + param += " -pix_fmt yuv420p"; + } + + if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) + { + var videoStream = state.VideoStream; + var isColorDepth10 = IsColorDepth10(state); + var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty; + var isNvdecDecoder = videoDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase); + + if (!isNvdecDecoder) + { + if (isColorDepth10 + && _mediaEncoder.SupportsHwaccel("opencl") + && encodingOptions.EnableTonemapping + && !string.IsNullOrEmpty(videoStream.VideoRange) + && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase)) + { + param += " -pix_fmt nv12"; + } + else + { + param += " -pix_fmt yuv420p"; + } + } + } + + if (string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) + { + param += " -pix_fmt nv21"; + } + var isVc1 = state.VideoStream != null && string.Equals(state.VideoStream.Codec, "vc1", StringComparison.OrdinalIgnoreCase); + var isLibX265 = string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase); - if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) || isLibX265) { - if (!string.IsNullOrEmpty(encodingOptions.H264Preset)) + if (!string.IsNullOrEmpty(encodingOptions.EncoderPreset)) { - param += "-preset " + encodingOptions.H264Preset; + param += " -preset " + encodingOptions.EncoderPreset; } else { - param += "-preset " + defaultH264Preset; + param += " -preset " + defaultPreset; } - if (encodingOptions.H264Crf >= 0 && encodingOptions.H264Crf <= 51) + int encodeCrf = encodingOptions.H264Crf; + if (isLibX265) { - param += " -crf " + encodingOptions.H264Crf.ToString(CultureInfo.InvariantCulture); + encodeCrf = encodingOptions.H265Crf; } - else + + if (encodeCrf >= 0 && encodeCrf <= 51) { - param += " -crf 23"; + param += " -crf " + encodeCrf.ToString(CultureInfo.InvariantCulture); } - } - - else if (string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase)) - { - param += "-preset fast"; + else + { + string defaultCrf = "23"; + if (isLibX265) + { + defaultCrf = "28"; + } - param += " -crf 28"; + param += " -crf " + defaultCrf; + } } - - // h264 (h264_qsv) - else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) // h264 (h264_qsv) + || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_qsv) { string[] valid_h264_qsv = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" }; - if (valid_h264_qsv.Contains(encodingOptions.H264Preset, StringComparer.OrdinalIgnoreCase)) + if (valid_h264_qsv.Contains(encodingOptions.EncoderPreset, StringComparer.OrdinalIgnoreCase)) { - param += "-preset " + encodingOptions.H264Preset; + param += " -preset " + encodingOptions.EncoderPreset; } else { - param += "-preset 7"; + param += " -preset 7"; } param += " -look_ahead 0"; - } - - // h264 (h264_nvenc) - else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc) + || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_nvenc) { - switch (encodingOptions.H264Preset) + switch (encodingOptions.EncoderPreset) { case "veryslow": - param += "-preset slow"; //lossless is only supported on maxwell and newer(2014+) + param += " -preset slow"; // lossless is only supported on maxwell and newer(2014+) break; case "slow": case "slower": - param += "-preset slow"; + param += " -preset slow"; break; case "medium": - param += "-preset medium"; + param += " -preset medium"; break; case "fast": @@ -693,17 +1138,61 @@ namespace MediaBrowser.Controller.MediaEncoding case "veryfast": case "superfast": case "ultrafast": - param += "-preset fast"; + param += " -preset fast"; break; default: - param += "-preset default"; + param += " -preset default"; break; } } + else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf) + || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_amf) + { + switch (encodingOptions.EncoderPreset) + { + case "veryslow": + case "slow": + case "slower": + param += " -quality quality"; + break; + + case "medium": + param += " -quality balanced"; + break; + + case "fast": + case "faster": + case "veryfast": + case "superfast": + case "ultrafast": + param += " -quality speed"; + break; + + default: + param += " -quality speed"; + break; + } + + var videoStream = state.VideoStream; + var isColorDepth10 = IsColorDepth10(state); - // webm - else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) + if (isColorDepth10 + && _mediaEncoder.SupportsHwaccel("opencl") + && encodingOptions.EnableTonemapping + && !string.IsNullOrEmpty(videoStream.VideoRange) + && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase)) + { + // Enhance workload when tone mapping with AMF on some APUs + param += " -preanalysis true"; + } + + if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) + { + param += " -header_insertion_mode gop -gops_per_idr 1"; + } + } + else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // vp8 { // Values 0-3, 0 being highest quality but slower var profileScore = 0; @@ -723,27 +1212,74 @@ namespace MediaBrowser.Controller.MediaEncoding profileScore = Math.Min(profileScore, 2); // http://www.webmproject.org/docs/encoder-parameters/ - param += string.Format("-speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}", - profileScore.ToString(_usCulture), + param += string.Format( + CultureInfo.InvariantCulture, + " -speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}", + profileScore.ToString(CultureInfo.InvariantCulture), crf, qmin, qmax); } + else if (string.Equals(videoEncoder, "libvpx-vp9", StringComparison.OrdinalIgnoreCase)) // vp9 + { + // When `-deadline` is set to `good` or `best`, `-cpu-used` ranges from 0-5. + // When `-deadline` is set to `realtime`, `-cpu-used` ranges from 0-15. + // Resources: + // * https://trac.ffmpeg.org/wiki/Encode/VP9 + // * https://superuser.com/questions/1586934 + // * https://developers.google.com/media/vp9 + param += encodingOptions.EncoderPreset switch + { + "veryslow" => " -deadline best -cpu-used 0", + "slower" => " -deadline best -cpu-used 2", + "slow" => " -deadline best -cpu-used 3", + "medium" => " -deadline good -cpu-used 0", + "fast" => " -deadline good -cpu-used 1", + "faster" => " -deadline good -cpu-used 2", + "veryfast" => " -deadline good -cpu-used 3", + "superfast" => " -deadline good -cpu-used 4", + "ultrafast" => " -deadline good -cpu-used 5", + _ => " -deadline good -cpu-used 1" + }; + + // TODO: until VP9 gets its own CRF setting, base CRF on H.265. + int h265Crf = encodingOptions.H265Crf; + int defaultVp9Crf = 31; + if (h265Crf >= 0 && h265Crf <= 51) + { + // This conversion factor is chosen to match the default CRF for H.265 to the + // recommended 1080p CRF from Google. The factor also maps the logarithmic CRF + // scale of x265 [0, 51] to that of VP9 [0, 63] relatively well. + + // Resources: + // * https://developers.google.com/media/vp9/settings/vod + const float H265ToVp9CrfConversionFactor = 1.12F; + var vp9Crf = Convert.ToInt32(h265Crf * H265ToVp9CrfConversionFactor); + + // Encoder allows for CRF values in the range [0, 63]. + vp9Crf = Math.Clamp(vp9Crf, 0, 63); + + param += FormattableString.Invariant($" -crf {vp9Crf}"); + } + else + { + param += FormattableString.Invariant($" -crf {defaultVp9Crf}"); + } + + param += " -row-mt 1 -profile 1"; + } else if (string.Equals(videoEncoder, "mpeg4", StringComparison.OrdinalIgnoreCase)) { - param += "-mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2"; + param += " -mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2"; } - - // asf/wmv - else if (string.Equals(videoEncoder, "wmv2", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(videoEncoder, "wmv2", StringComparison.OrdinalIgnoreCase)) // asf/wmv { - param += "-qmin 2"; + param += " -qmin 2"; } - else if (string.Equals(videoEncoder, "msmpeg4", StringComparison.OrdinalIgnoreCase)) { - param += "-mbd 2"; + param += " -mbd 2"; } param += GetVideoBitrateParam(state, videoEncoder); @@ -751,29 +1287,92 @@ namespace MediaBrowser.Controller.MediaEncoding var framerate = GetFramerateParam(state); if (framerate.HasValue) { - param += string.Format(" -r {0}", framerate.Value.ToString(_usCulture)); + param += string.Format(CultureInfo.InvariantCulture, " -r {0}", framerate.Value.ToString(CultureInfo.InvariantCulture)); } var targetVideoCodec = state.ActualOutputVideoCodec; + if (string.Equals(targetVideoCodec, "h265", StringComparison.OrdinalIgnoreCase) + || string.Equals(targetVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)) + { + targetVideoCodec = "hevc"; + } - var request = state.BaseRequest; - var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault(); + var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault() ?? string.Empty; + profile = Regex.Replace(profile, @"\s+", string.Empty); + + // We only transcode to HEVC 8-bit for now, force Main Profile. + if (profile.Contains("main10", StringComparison.OrdinalIgnoreCase) + || profile.Contains("mainstill", StringComparison.OrdinalIgnoreCase)) + { + profile = "main"; + } + + // Extended Profile is not supported by any known h264 encoders, force Main Profile. + if (profile.Contains("extended", StringComparison.OrdinalIgnoreCase)) + { + profile = "main"; + } + + // Only libx264 support encoding H264 High 10 Profile, otherwise force High Profile. + if (!string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) + && profile.Contains("high10", StringComparison.OrdinalIgnoreCase)) + { + profile = "high"; + } + + // h264_vaapi does not support Baseline profile, force Constrained Baseline in this case, + // which is compatible (and ugly). + if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + && profile.Contains("baseline", StringComparison.OrdinalIgnoreCase)) + { + profile = "constrained_baseline"; + } + + // libx264, h264_qsv and h264_nvenc does not support Constrained Baseline profile, force Baseline in this case. + if ((string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)) + && profile.Contains("baseline", StringComparison.OrdinalIgnoreCase)) + { + profile = "baseline"; + } + + // libx264, h264_qsv, h264_nvenc and h264_vaapi does not support Constrained High profile, force High in this case. + if ((string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + && profile.Contains("high", StringComparison.OrdinalIgnoreCase)) + { + profile = "high"; + } + + if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) + && profile.Contains("baseline", StringComparison.OrdinalIgnoreCase)) + { + profile = "constrained_baseline"; + } - // vaapi does not support Baseline profile, force Constrained Baseline in this case, - // which is compatible (and ugly) - if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) && - profile != null && profile.ToLowerInvariant().Contains("baseline")) + if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) + && profile.Contains("constrainedhigh", StringComparison.OrdinalIgnoreCase)) { - profile = "constrained_baseline"; + profile = "constrained_high"; + } + + // Currently hevc_amf only support encoding HEVC Main Profile, otherwise force Main Profile. + if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) + && profile.Contains("main10", StringComparison.OrdinalIgnoreCase)) + { + profile = "main"; } if (!string.IsNullOrEmpty(profile)) { - if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) && - !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) { // not supported by h264_omx - param += " -profile:v " + profile; + param += " -profile:v:0 " + profile; } } @@ -781,53 +1380,35 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(level)) { - level = NormalizeTranscodingLevel(state.OutputVideoCodec, level); - - // h264_qsv and h264_nvenc expect levels to be expressed as a decimal. libx264 supports decimal and non-decimal format - // also needed for libx264 due to https://trac.ffmpeg.org/ticket/3307 - if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) || - string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)) - { - switch (level) - { - case "30": - param += " -level 3.0"; - break; - case "31": - param += " -level 3.1"; - break; - case "32": - param += " -level 3.2"; - break; - case "40": - param += " -level 4.0"; - break; - case "41": - param += " -level 4.1"; - break; - case "42": - param += " -level 4.2"; - break; - case "50": - param += " -level 5.0"; - break; - case "51": - param += " -level 5.1"; - break; - case "52": - param += " -level 5.2"; - break; - default: - param += " -level " + level; - break; - } - } - // nvenc doesn't decode with param -level set ?! - else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)) - { - //param += ""; - } - else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)) + level = NormalizeTranscodingLevel(state, level); + + // libx264, QSV, AMF, VAAPI can adjust the given level to match the output. + if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)) + { + param += " -level " + level; + } + else if (string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) + { + // hevc_qsv use -level 51 instead of -level 153. + if (double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out double hevcLevel)) + { + param += " -level " + (hevcLevel / 3); + } + } + else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) + { + param += " -level " + level; + } + else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) + { + // level option may cause NVENC to fail. + // NVENC cannot adjust the given level, just throw an error. + } + else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) + || !string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase)) { param += " -level " + level; } @@ -838,17 +1419,13 @@ namespace MediaBrowser.Controller.MediaEncoding param += " -x264opts:0 subme=0:me_range=4:rc_lookahead=10:me=dia:no_chroma_me:8x8dct=0:partitions=none"; } - if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) && - !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) && - !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) && - !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase)) { - param = "-pix_fmt yuv420p " + param; - } - - if (string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) - { - param = "-pix_fmt nv21 " + param; + // 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. + // TODO: set fine tuned params. + param += " -x265-params:0 no-info=1"; } return param; @@ -863,12 +1440,10 @@ namespace MediaBrowser.Controller.MediaEncoding return false; } - if (videoStream.IsInterlaced) + if (videoStream.IsInterlaced + && state.DeInterlace(videoStream.Codec, false)) { - if (state.DeInterlace(videoStream.Codec, false)) - { - return false; - } + return false; } if (videoStream.IsAnamorphic ?? false) @@ -880,24 +1455,23 @@ namespace MediaBrowser.Controller.MediaEncoding } // Can't stream copy if we're burning in subtitles - if (request.SubtitleStreamIndex.HasValue) + if (request.SubtitleStreamIndex.HasValue + && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) { - if (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) - { - return false; - } + return false; } - if (string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) + if (string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase) + && videoStream.IsAVC.HasValue + && !videoStream.IsAVC.Value + && request.RequireAvc) { - if (videoStream.IsAVC.HasValue && !videoStream.IsAVC.Value && request.RequireAvc) - { - return false; - } + return false; } // Source and target codecs must match - if (string.IsNullOrEmpty(videoStream.Codec) || !state.SupportedVideoCodecs.Contains(videoStream.Codec, StringComparer.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(videoStream.Codec) + || !state.SupportedVideoCodecs.Contains(videoStream.Codec, StringComparer.OrdinalIgnoreCase)) { return false; } @@ -909,12 +1483,13 @@ namespace MediaBrowser.Controller.MediaEncoding { if (string.IsNullOrEmpty(videoStream.Profile)) { - //return false; + // return false; } var requestedProfile = requestedProfiles[0]; // strip spaces because they may be stripped out on the query string as well - if (!string.IsNullOrEmpty(videoStream.Profile) && !requestedProfiles.Contains(videoStream.Profile.Replace(" ", ""), StringComparer.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(videoStream.Profile) + && !requestedProfiles.Contains(videoStream.Profile.Replace(" ", string.Empty, StringComparison.Ordinal), StringComparer.OrdinalIgnoreCase)) { var currentScore = GetVideoProfileScore(videoStream.Profile); var requestedScore = GetVideoProfileScore(requestedProfile); @@ -927,21 +1502,17 @@ namespace MediaBrowser.Controller.MediaEncoding } // Video width must fall within requested value - if (request.MaxWidth.HasValue) + if (request.MaxWidth.HasValue + && (!videoStream.Width.HasValue || videoStream.Width.Value > request.MaxWidth.Value)) { - if (!videoStream.Width.HasValue || videoStream.Width.Value > request.MaxWidth.Value) - { - return false; - } + return false; } // Video height must fall within requested value - if (request.MaxHeight.HasValue) + if (request.MaxHeight.HasValue + && (!videoStream.Height.HasValue || videoStream.Height.Value > request.MaxHeight.Value)) { - if (!videoStream.Height.HasValue || videoStream.Height.Value > request.MaxHeight.Value) - { - return false; - } + return false; } // Video framerate must fall within requested value @@ -957,12 +1528,10 @@ namespace MediaBrowser.Controller.MediaEncoding } // Video bitrate must fall within requested value - if (request.VideoBitRate.HasValue) + if (request.VideoBitRate.HasValue + && (!videoStream.BitRate.HasValue || videoStream.BitRate.Value > request.VideoBitRate.Value)) { - if (!videoStream.BitRate.HasValue || videoStream.BitRate.Value > request.VideoBitRate.Value) - { - return false; - } + return false; } var maxBitDepth = state.GetRequestedVideoBitDepth(videoStream.Codec); @@ -975,35 +1544,31 @@ namespace MediaBrowser.Controller.MediaEncoding } var maxRefFrames = state.GetRequestedMaxRefFrames(videoStream.Codec); - if (maxRefFrames.HasValue) + if (maxRefFrames.HasValue + && videoStream.RefFrames.HasValue && videoStream.RefFrames.Value > maxRefFrames.Value) { - if (videoStream.RefFrames.HasValue && videoStream.RefFrames.Value > maxRefFrames.Value) - { - return false; - } + return false; } // If a specific level was requested, the source must match or be less than var level = state.GetRequestedLevel(videoStream.Codec); - if (!string.IsNullOrEmpty(level)) + if (!string.IsNullOrEmpty(level) + && double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out var requestLevel)) { - if (double.TryParse(level, NumberStyles.Any, _usCulture, out var requestLevel)) + if (!videoStream.Level.HasValue) { - if (!videoStream.Level.HasValue) - { - //return false; - } + // return false; + } - if (videoStream.Level.HasValue && videoStream.Level.Value > requestLevel) - { - return false; - } + if (videoStream.Level.HasValue && videoStream.Level.Value > requestLevel) + { + return false; } } - if (string.Equals(state.InputContainer, "avi", StringComparison.OrdinalIgnoreCase) && - string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase) && - !(videoStream.IsAVC ?? false)) + if (string.Equals(state.InputContainer, "avi", StringComparison.OrdinalIgnoreCase) + && string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase) + && !(videoStream.IsAVC ?? false)) { // see Coach S01E01 - Kelly and the Professor(0).avi return false; @@ -1012,7 +1577,7 @@ namespace MediaBrowser.Controller.MediaEncoding return request.EnableAutoStreamCopy; } - public bool CanStreamCopyAudio(EncodingJobInfo state, MediaStream audioStream, string[] supportedAudioCodecs) + public bool CanStreamCopyAudio(EncodingJobInfo state, MediaStream audioStream, IEnumerable<string> supportedAudioCodecs) { var request = state.BaseRequest; @@ -1022,16 +1587,16 @@ namespace MediaBrowser.Controller.MediaEncoding } var maxBitDepth = state.GetRequestedAudioBitDepth(audioStream.Codec); - if (maxBitDepth.HasValue) + if (maxBitDepth.HasValue + && audioStream.BitDepth.HasValue + && audioStream.BitDepth.Value > maxBitDepth.Value) { - if (audioStream.BitDepth.HasValue && audioStream.BitDepth.Value > maxBitDepth.Value) - { - return false; - } + return false; } // Source and target codecs must match - if (string.IsNullOrEmpty(audioStream.Codec) || !supportedAudioCodecs.Contains(audioStream.Codec, StringComparer.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(audioStream.Codec) + || !supportedAudioCodecs.Contains(audioStream.Codec, StringComparer.OrdinalIgnoreCase)) { return false; } @@ -1044,6 +1609,7 @@ namespace MediaBrowser.Controller.MediaEncoding { return false; } + if (audioStream.Channels.Value > channels.Value) { return false; @@ -1057,6 +1623,7 @@ namespace MediaBrowser.Controller.MediaEncoding { return false; } + if (audioStream.SampleRate.Value > request.AudioSampleRate.Value) { return false; @@ -1070,6 +1637,7 @@ namespace MediaBrowser.Controller.MediaEncoding { return false; } + if (audioStream.BitRate.Value > request.AudioBitRate.Value) { return false; @@ -1083,14 +1651,21 @@ namespace MediaBrowser.Controller.MediaEncoding { var bitrate = request.VideoBitRate; - // If specific values were requested, then force the caller to supply a bitrate as well - if (request.Height.HasValue && request.Width.HasValue) - { - return bitrate; - } - if (videoStream != null) { + var isUpscaling = request.Height.HasValue + && videoStream.Height.HasValue + && request.Height.Value > videoStream.Height.Value + && request.Width.HasValue + && videoStream.Width.HasValue + && request.Width.Value > videoStream.Width.Value; + + // Don't allow bitrate increases unless upscaling + if (!isUpscaling && bitrate.HasValue && videoStream.BitRate.HasValue) + { + bitrate = GetMinBitrate(videoStream.BitRate.Value, bitrate.Value); + } + if (bitrate.HasValue) { var inputVideoCodec = videoStream.Codec; @@ -1107,14 +1682,32 @@ namespace MediaBrowser.Controller.MediaEncoding return bitrate; } + private int GetMinBitrate(int sourceBitrate, int requestedBitrate) + { + // these values were chosen from testing to improve low bitrate streams + if (sourceBitrate <= 2000000) + { + sourceBitrate = Convert.ToInt32(sourceBitrate * 2.5); + } + else if (sourceBitrate <= 3000000) + { + sourceBitrate *= 2; + } + + var bitrate = Math.Min(sourceBitrate, requestedBitrate); + + return bitrate; + } + private static double GetVideoBitrateScaleFactor(string codec) { - if (StringHelper.EqualsIgnoreCase(codec, "h265") || - StringHelper.EqualsIgnoreCase(codec, "hevc") || - StringHelper.EqualsIgnoreCase(codec, "vp9")) + if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase)) { - return .5; + return .6; } + return 1; } @@ -1141,39 +1734,74 @@ namespace MediaBrowser.Controller.MediaEncoding scaleFactor = Math.Max(scaleFactor, 2); } - var newBitrate = scaleFactor * bitrate; - - return Convert.ToInt32(newBitrate); + return Convert.ToInt32(scaleFactor * bitrate); } public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream) { - if (request.AudioBitRate.HasValue) + return GetAudioBitrateParam(request.AudioBitRate, request.AudioCodec, audioStream); + } + + public int? GetAudioBitrateParam(int? audioBitRate, string audioCodec, MediaStream audioStream) + { + if (audioStream == null) { - // Make sure we don't request a bitrate higher than the source - var currentBitrate = audioStream == null ? request.AudioBitRate.Value : audioStream.BitRate ?? request.AudioBitRate.Value; + return null; + } - // Don't encode any higher than this - return Math.Min(384000, request.AudioBitRate.Value); - //return Math.Min(currentBitrate, request.AudioBitRate.Value); + if (audioBitRate.HasValue && string.IsNullOrEmpty(audioCodec)) + { + return Math.Min(384000, audioBitRate.Value); } - return null; + if (audioBitRate.HasValue && !string.IsNullOrEmpty(audioCodec)) + { + if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase) + || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase) + || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase) + || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase)) + { + if ((audioStream.Channels ?? 0) >= 6) + { + return Math.Min(640000, audioBitRate.Value); + } + + return Math.Min(384000, audioBitRate.Value); + } + + if (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase) + || string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase)) + { + if ((audioStream.Channels ?? 0) >= 6) + { + return Math.Min(3584000, audioBitRate.Value); + } + + return Math.Min(1536000, audioBitRate.Value); + } + } + + // Empty bitrate area is not allow on iOS + // Default audio bitrate to 128K if it is not being requested + // https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options + return 128000; } - public string GetAudioFilterParam(EncodingJobInfo state, EncodingOptions encodingOptions, bool isHls) + public string GetAudioFilterParam(EncodingJobInfo state, EncodingOptions encodingOptions) { var channels = state.OutputAudioChannels; var filters = new List<string>(); // Boost volume to 200% when downsampling from 6ch to 2ch - if (channels.HasValue && channels.Value <= 2) + if (channels.HasValue + && channels.Value <= 2 + && state.AudioStream != null + && state.AudioStream.Channels.HasValue + && state.AudioStream.Channels.Value > 5 + && !encodingOptions.DownMixAudioBoost.Equals(1)) { - if (state.AudioStream != null && state.AudioStream.Channels.HasValue && state.AudioStream.Channels.Value > 5 && !encodingOptions.DownMixAudioBoost.Equals(1)) - { - filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(_usCulture)); - } + filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture)); } var isCopyingTimestamps = state.CopyTimestamps || state.TranscodingType != TranscodingJobType.Progressive; @@ -1181,19 +1809,23 @@ namespace MediaBrowser.Controller.MediaEncoding { var seconds = TimeSpan.FromTicks(state.StartTimeTicks ?? 0).TotalSeconds; - filters.Add(string.Format("asetpts=PTS-{0}/TB", Math.Round(seconds).ToString(_usCulture))); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "asetpts=PTS-{0}/TB", + Math.Round(seconds))); } if (filters.Count > 0) { - return "-af \"" + string.Join(",", filters.ToArray()) + "\""; + return " -af \"" + string.Join(',', filters) + "\""; } return string.Empty; } /// <summary> - /// Gets the number of audio channels to specify on the command line + /// Gets the number of audio channels to specify on the command line. /// </summary> /// <param name="state">The state.</param> /// <param name="audioStream">The audio stream.</param> @@ -1201,38 +1833,45 @@ namespace MediaBrowser.Controller.MediaEncoding /// <returns>System.Nullable{System.Int32}.</returns> public int? GetNumAudioChannelsParam(EncodingJobInfo state, MediaStream audioStream, string outputAudioCodec) { + if (audioStream == null) + { + return null; + } + var request = state.BaseRequest; - var inputChannels = audioStream == null - ? null - : audioStream.Channels; + var inputChannels = audioStream.Channels; if (inputChannels <= 0) { inputChannels = null; } - int? transcoderChannelLimit = null; var codec = outputAudioCodec ?? string.Empty; + int? transcoderChannelLimit; if (codec.IndexOf("wma", StringComparison.OrdinalIgnoreCase) != -1) { // wmav2 currently only supports two channel output transcoderChannelLimit = 2; } - else if (codec.IndexOf("mp3", StringComparison.OrdinalIgnoreCase) != -1) { // libmp3lame currently only supports two channel output transcoderChannelLimit = 2; } + else if (codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1) + { + // aac is able to handle 8ch(7.1 layout) + transcoderChannelLimit = 8; + } else { // If we don't have any media info then limit it to 6 to prevent encoding errors due to asking for too many channels transcoderChannelLimit = 6; } - var isTranscodingAudio = !string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase); + var isTranscodingAudio = !IsCopyCodec(codec); int? resultChannels = state.GetRequestedAudioChannels(codec); if (isTranscodingAudio) @@ -1254,6 +1893,16 @@ namespace MediaBrowser.Controller.MediaEncoding : transcoderChannelLimit.Value; } + // Avoid transcoding to audio channels other than 1ch, 2ch, 6ch (5.1 layout) and 8ch (7.1 layout). + // https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices + if (isTranscodingAudio + && state.TranscodingType != TranscodingJobType.Progressive + && resultChannels.HasValue + && ((resultChannels.Value > 2 && resultChannels.Value < 6) || resultChannels.Value == 7)) + { + resultChannels = 2; + } + return resultChannels; } @@ -1263,6 +1912,7 @@ namespace MediaBrowser.Controller.MediaEncoding { return val2; } + if (!val2.HasValue) { return val1; @@ -1299,7 +1949,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (time > 0) { - return string.Format("-ss {0}", _mediaEncoder.GetTimeParameter(time)); + return string.Format(CultureInfo.InvariantCulture, "-ss {0}", _mediaEncoder.GetTimeParameter(time)); } return string.Empty; @@ -1336,7 +1986,10 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.VideoStream != null) { - args += string.Format("-map 0:{0}", state.VideoStream.Index); + args += string.Format( + CultureInfo.InvariantCulture, + "-map 0:{0}", + state.VideoStream.Index); } else { @@ -1346,9 +1999,11 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.AudioStream != null) { - args += string.Format(" -map 0:{0}", state.AudioStream.Index); + args += string.Format( + CultureInfo.InvariantCulture, + " -map 0:{0}", + state.AudioStream.Index); } - else { args += " -map -0:a"; @@ -1361,7 +2016,10 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (subtitleMethod == SubtitleDeliveryMethod.Embed) { - args += string.Format(" -map 0:{0}", state.SubtitleStream.Index); + args += string.Format( + CultureInfo.InvariantCulture, + " -map 0:{0}", + state.SubtitleStream.Index); } else if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream) { @@ -1372,7 +2030,7 @@ namespace MediaBrowser.Controller.MediaEncoding } /// <summary> - /// Determines which stream will be used for playback + /// Determines which stream will be used for playback. /// </summary> /// <param name="allStream">All stream.</param> /// <param name="desiredIndex">Index of the desired.</param> @@ -1404,56 +2062,90 @@ namespace MediaBrowser.Controller.MediaEncoding } /// <summary> - /// Gets the internal graphical subtitle param. + /// Gets the graphical subtitle parameter. /// </summary> - public string GetGraphicalSubtitleParam(EncodingJobInfo state, EncodingOptions options, string outputVideoCodec) + /// <param name="state">Encoding state.</param> + /// <param name="options">Encoding options.</param> + /// <param name="outputVideoCodec">Video codec to use.</param> + /// <returns>Graphical subtitle parameter.</returns> + public string GetGraphicalSubtitleParam( + EncodingJobInfo state, + EncodingOptions options, + string outputVideoCodec) { - var outputSizeParam = string.Empty; + outputVideoCodec ??= string.Empty; + var outputSizeParam = ReadOnlySpan<char>.Empty; var request = state.BaseRequest; - // Add resolution params, if specified - if (request.Width.HasValue || request.Height.HasValue || request.MaxHeight.HasValue || request.MaxWidth.HasValue) + outputSizeParam = GetOutputSizeParamInternal(state, options, outputVideoCodec); + + var videoSizeParam = string.Empty; + var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options) ?? string.Empty; + var isLinux = OperatingSystem.IsLinux(); + + var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; + var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1; + var isVaapiHevcEncoder = outputVideoCodec.IndexOf("hevc_vaapi", StringComparison.OrdinalIgnoreCase) != -1; + var isQsvH264Encoder = outputVideoCodec.Contains("h264_qsv", StringComparison.OrdinalIgnoreCase); + var isQsvHevcEncoder = outputVideoCodec.Contains("hevc_qsv", StringComparison.OrdinalIgnoreCase); + var isNvdecDecoder = videoDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase); + var isNvencEncoder = outputVideoCodec.Contains("nvenc", StringComparison.OrdinalIgnoreCase); + var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder); + var isTonemappingSupportedOnQsv = string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isQsvH264Encoder || isQsvHevcEncoder); + var isOpenclTonemappingSupported = IsOpenclTonemappingSupported(state, options); + var isVppTonemappingSupported = IsVppTonemappingSupported(state, options); + + var mediaEncoderVersion = _mediaEncoder.GetMediaEncoderVersion(); + var isCudaOverlaySupported = _mediaEncoder.SupportsFilter("overlay_cuda") && mediaEncoderVersion != null && mediaEncoderVersion >= _minVersionForCudaOverlay; + var isCudaFormatConversionSupported = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.ScaleCudaFormat); + + // Tonemapping and burn-in graphical subtitles requires overlay_vaapi. + // But it's still in ffmpeg mailing list. Disable it for now. + if (isTonemappingSupportedOnVaapi && isOpenclTonemappingSupported && !isVppTonemappingSupported) + { + return GetOutputSizeParam(state, options, outputVideoCodec); + } + + // Setup subtitle scaling + if (state.VideoStream != null && state.VideoStream.Width.HasValue && state.VideoStream.Height.HasValue) { - outputSizeParam = GetOutputSizeParam(state, options, outputVideoCodec).TrimEnd('"'); + // Adjust the size of graphical subtitles to fit the video stream. + var videoStream = state.VideoStream; + var inputWidth = videoStream.Width; + var inputHeight = videoStream.Height; + var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight); - if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + if (width.HasValue && height.HasValue) { - var index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase); - if (index != -1) - { - outputSizeParam = "," + outputSizeParam.Substring(index); - } + videoSizeParam = string.Format( + CultureInfo.InvariantCulture, + "scale={0}x{1}", + width.Value, + height.Value); } - else + + if (!string.IsNullOrEmpty(videoSizeParam) + && !(isTonemappingSupportedOnQsv && isVppTonemappingSupported)) { - var index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase); - if (index != -1) + // upload graphical subtitle to QSV + if (isLinux && (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(outputVideoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase))) { - outputSizeParam = "," + outputSizeParam.Substring(index); + videoSizeParam += ",hwupload=extra_hw_frames=64"; } } - } - - if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) && outputSizeParam.Length == 0) - { - outputSizeParam = ",format=nv12|vaapi,hwupload"; - // Add parameters to use VAAPI with burn-in subttiles (GH issue #642) - if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) { - outputSizeParam += ",hwmap=mode=read+write+direct"; + if (!string.IsNullOrEmpty(videoSizeParam)) + { + // upload graphical subtitle to cuda + if (isNvdecDecoder && isNvencEncoder && isCudaOverlaySupported && isCudaFormatConversionSupported) + { + videoSizeParam += ",hwupload_cuda"; + } } } - var videoSizeParam = string.Empty; - - if (state.VideoStream != null && state.VideoStream.Width.HasValue && state.VideoStream.Height.HasValue) - { - videoSizeParam = string.Format("scale={0}:{1}", state.VideoStream.Width.Value.ToString(_usCulture), state.VideoStream.Height.Value.ToString(_usCulture)); - - videoSizeParam += ":force_original_aspect_ratio=decrease"; - } - var mapPrefix = state.SubtitleStream.IsExternal ? 1 : 0; @@ -1462,15 +2154,88 @@ namespace MediaBrowser.Controller.MediaEncoding ? 0 : state.SubtitleStream.Index; - return string.Format(" -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay{3}\"", - mapPrefix.ToString(_usCulture), - subtitleStreamIndex.ToString(_usCulture), - state.VideoStream.Index.ToString(_usCulture), - outputSizeParam, + // Setup default filtergraph utilizing FFMpeg overlay() and FFMpeg scale() (see the return of this function for index reference) + // Always put the scaler before the overlay for better performance + var retStr = outputSizeParam.IsEmpty + ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\"" + : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\""; + + // When the input may or may not be hardware VAAPI decodable + if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + || string.Equals(outputVideoCodec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)) + { + /* + [base]: HW scaling video to OutputSize + [sub]: SW scaling subtitle to FixedOutputSize + [base][sub]: SW overlay + */ + retStr = outputSizeParam.IsEmpty + ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]hwdownload[base];[base][sub]overlay,format=nv12,hwupload\"" + : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3},hwdownload[base];[base][sub]overlay,format=nv12,hwupload\""; + } + + // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first + else if (_mediaEncoder.SupportsHwaccel("vaapi") && videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1 + && (string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase) + || string.Equals(outputVideoCodec, "libx265", StringComparison.OrdinalIgnoreCase))) + { + /* + [base]: SW scaling video to OutputSize + [sub]: SW scaling subtitle to FixedOutputSize + [base][sub]: SW overlay + */ + retStr = outputSizeParam.IsEmpty + ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\"" + : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\""; + } + else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(outputVideoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) + { + /* + QSV in FFMpeg can now setup hardware overlay for transcodes. + For software decoding and hardware encoding option, frames must be hwuploaded into hardware + with fixed frame size. + Currently only supports linux. + */ + if (isTonemappingSupportedOnQsv && isVppTonemappingSupported) + { + retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3},hwdownload,format=nv12[base];[base][sub]overlay\""; + } + else if (isLinux) + { + retStr = outputSizeParam.IsEmpty + ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv\"" + : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_qsv\""; + } + } + else if (isNvdecDecoder && isNvencEncoder) + { + if (isCudaOverlaySupported && isCudaFormatConversionSupported) + { + retStr = outputSizeParam.IsEmpty + ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]scale_cuda=format=yuv420p[base];[base][sub]overlay_cuda\"" + : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_cuda\""; + } + else + { + retStr = outputSizeParam.IsEmpty + ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay,format=nv12|yuv420p,hwupload_cuda\"" + : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay,format=nv12|yuv420p,hwupload_cuda\""; + } + } + + return string.Format( + CultureInfo.InvariantCulture, + retStr, + mapPrefix, + subtitleStreamIndex, + state.VideoStream.Index, + outputSizeParam.ToString(), videoSizeParam); } - private ValueTuple<int?, int?> GetFixedOutputSize(int? videoWidth, + public static (int? width, int? height) GetFixedOutputSize( + int? videoWidth, int? videoHeight, int? requestedWidth, int? requestedHeight, @@ -1479,15 +2244,16 @@ namespace MediaBrowser.Controller.MediaEncoding { if (!videoWidth.HasValue && !requestedWidth.HasValue) { - return new ValueTuple<int?, int?>(null, null); + return (null, null); } + if (!videoHeight.HasValue && !requestedHeight.HasValue) { - return new ValueTuple<int?, int?>(null, null); + return (null, null); } - decimal inputWidth = Convert.ToDecimal(videoWidth ?? requestedWidth); - decimal inputHeight = Convert.ToDecimal(videoHeight ?? requestedHeight); + decimal inputWidth = Convert.ToDecimal(videoWidth ?? requestedWidth, CultureInfo.InvariantCulture); + decimal inputHeight = Convert.ToDecimal(videoHeight ?? requestedHeight, CultureInfo.InvariantCulture); decimal outputWidth = requestedWidth.HasValue ? Convert.ToDecimal(requestedWidth.Value) : inputWidth; decimal outputHeight = requestedHeight.HasValue ? Convert.ToDecimal(requestedHeight.Value) : inputHeight; decimal maximumWidth = requestedMaxWidth.HasValue ? Convert.ToDecimal(requestedMaxWidth.Value) : outputWidth; @@ -1503,10 +2269,13 @@ namespace MediaBrowser.Controller.MediaEncoding outputWidth = 2 * Math.Truncate(outputWidth / 2); outputHeight = 2 * Math.Truncate(outputHeight / 2); - return new ValueTuple<int?, int?>(Convert.ToInt32(outputWidth), Convert.ToInt32(outputHeight)); + return (Convert.ToInt32(outputWidth), Convert.ToInt32(outputHeight)); } - public List<string> GetScalingFilters(int? videoWidth, + public List<string> GetScalingFilters( + EncodingJobInfo state, + EncodingOptions options, + int? videoWidth, int? videoHeight, Video3DFormat? threedFormat, string videoDecoder, @@ -1517,25 +2286,140 @@ namespace MediaBrowser.Controller.MediaEncoding int? requestedMaxHeight) { var filters = new List<string>(); - var fixedOutputSize = GetFixedOutputSize(videoWidth, videoHeight, requestedWidth, requestedHeight, requestedMaxWidth, requestedMaxHeight); - - if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) && fixedOutputSize.Item1.HasValue && fixedOutputSize.Item2.HasValue) + var (width, height) = GetFixedOutputSize( + videoWidth, + videoHeight, + requestedWidth, + requestedHeight, + requestedMaxWidth, + requestedMaxHeight); + + if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) + && width.HasValue + && height.HasValue) { - // Work around vaapi's reduced scaling features - var scaler = "scale_vaapi"; - // Given the input dimensions (inputWidth, inputHeight), determine the output dimensions // (outputWidth, outputHeight). The user may request precise output dimensions or maximum // output dimensions. Output dimensions are guaranteed to be even. - var outputWidth = fixedOutputSize.Item1.Value; - var outputHeight = fixedOutputSize.Item2.Value; + var outputWidth = width.Value; + var outputHeight = height.Value; + var qsv_or_vaapi = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase); + var isDeintEnabled = state.DeInterlace("h264", true) + || state.DeInterlace("avc", true) + || state.DeInterlace("h265", true) + || state.DeInterlace("hevc", true); + + var isVaapiDecoder = videoDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); + var isVaapiH264Encoder = videoEncoder.Contains("h264_vaapi", StringComparison.OrdinalIgnoreCase); + var isVaapiHevcEncoder = videoEncoder.Contains("hevc_vaapi", StringComparison.OrdinalIgnoreCase); + var isQsvH264Encoder = videoEncoder.Contains("h264_qsv", StringComparison.OrdinalIgnoreCase); + var isQsvHevcEncoder = videoEncoder.Contains("hevc_qsv", StringComparison.OrdinalIgnoreCase); + var isOpenclTonemappingSupported = IsOpenclTonemappingSupported(state, options); + var isVppTonemappingSupported = IsVppTonemappingSupported(state, options); + var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder); + var isTonemappingSupportedOnQsv = string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isQsvH264Encoder || isQsvHevcEncoder); + var isP010PixFmtRequired = (isTonemappingSupportedOnVaapi && (isOpenclTonemappingSupported || isVppTonemappingSupported)) + || (isTonemappingSupportedOnQsv && isVppTonemappingSupported); + + var outputPixFmt = "format=nv12"; + if (isP010PixFmtRequired) + { + outputPixFmt = "format=p010"; + } + + if (isTonemappingSupportedOnQsv && isVppTonemappingSupported) + { + qsv_or_vaapi = false; + } + + if (!videoWidth.HasValue + || outputWidth != videoWidth.Value + || !videoHeight.HasValue + || outputHeight != videoHeight.Value) + { + // Force nv12 pixel format to enable 10-bit to 8-bit colour conversion. + // use vpp_qsv filter to avoid green bar when the fixed output size is requested. + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "{0}=w={1}:h={2}{3}{4}", + qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi", + outputWidth, + outputHeight, + ":" + outputPixFmt, + (qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty)); + } - if (!videoWidth.HasValue || outputWidth != videoWidth.Value || !videoHeight.HasValue || outputHeight != videoHeight.Value) + // Assert 10-bit is P010 so as we can avoid the extra scaler to get a bit more fps on high res HDR videos. + else if (!isP010PixFmtRequired) { - filters.Add(string.Format("{0}=w={1}:h={2}", scaler, outputWidth.ToString(_usCulture), outputHeight.ToString(_usCulture))); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "{0}={1}{2}", + qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi", + outputPixFmt, + (qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty)); } } - else if ((videoDecoder ?? string.Empty).IndexOf("_cuvid", StringComparison.OrdinalIgnoreCase) != -1 && fixedOutputSize.Item1.HasValue && fixedOutputSize.Item2.HasValue) + else if ((videoDecoder ?? string.Empty).Contains("cuda", StringComparison.OrdinalIgnoreCase) + && width.HasValue + && height.HasValue) + { + var outputWidth = width.Value; + var outputHeight = height.Value; + + var isNvencEncoder = videoEncoder.Contains("nvenc", StringComparison.OrdinalIgnoreCase); + var isOpenclTonemappingSupported = IsOpenclTonemappingSupported(state, options); + var isCudaTonemappingSupported = IsCudaTonemappingSupported(state, options); + var isTonemappingSupportedOnNvenc = string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase); + var mediaEncoderVersion = _mediaEncoder.GetMediaEncoderVersion(); + var isCudaOverlaySupported = _mediaEncoder.SupportsFilter("overlay_cuda") && mediaEncoderVersion != null && mediaEncoderVersion >= _minVersionForCudaOverlay; + var isCudaFormatConversionSupported = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.ScaleCudaFormat); + var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + + var outputPixFmt = string.Empty; + if (isCudaFormatConversionSupported) + { + outputPixFmt = (hasGraphicalSubs && isCudaOverlaySupported && isNvencEncoder) + ? "format=yuv420p" + : "format=nv12"; + if ((isOpenclTonemappingSupported || isCudaTonemappingSupported) + && isTonemappingSupportedOnNvenc) + { + outputPixFmt = "format=p010"; + } + } + + if (!videoWidth.HasValue + || outputWidth != videoWidth.Value + || !videoHeight.HasValue + || outputHeight != videoHeight.Value) + { + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "scale_cuda=w={0}:h={1}{2}", + outputWidth, + outputHeight, + isCudaFormatConversionSupported ? (":" + outputPixFmt) : string.Empty)); + } + else if (isCudaFormatConversionSupported) + { + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "scale_cuda={0}", + outputPixFmt)); + } + } + else if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1 + && width.HasValue + && height.HasValue) { // Nothing to do, it's handled as an input resize filter } @@ -1548,10 +2432,15 @@ namespace MediaBrowser.Controller.MediaEncoding { if (isExynosV4L2) { - var widthParam = requestedWidth.Value.ToString(_usCulture); - var heightParam = requestedHeight.Value.ToString(_usCulture); - - filters.Add(string.Format("scale=trunc({0}/64)*64:trunc({1}/2)*2", widthParam, heightParam)); + var widthParam = requestedWidth.Value.ToString(CultureInfo.InvariantCulture); + var heightParam = requestedHeight.Value.ToString(CultureInfo.InvariantCulture); + + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "scale=trunc({0}/64)*64:trunc({1}/2)*2", + widthParam, + heightParam)); } else { @@ -1562,16 +2451,26 @@ namespace MediaBrowser.Controller.MediaEncoding // If Max dimensions were supplied, for width selects lowest even number between input width and width req size and selects lowest even number from in width*display aspect and requested size else if (requestedMaxWidth.HasValue && requestedMaxHeight.HasValue) { - var maxWidthParam = requestedMaxWidth.Value.ToString(_usCulture); - var maxHeightParam = requestedMaxHeight.Value.ToString(_usCulture); + var maxWidthParam = requestedMaxWidth.Value.ToString(CultureInfo.InvariantCulture); + var maxHeightParam = requestedMaxHeight.Value.ToString(CultureInfo.InvariantCulture); if (isExynosV4L2) { - filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/64)*64:trunc(min(max(iw/dar\\,ih)\\,min({0}/dar\\,{1}))/2)*2", maxWidthParam, maxHeightParam)); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "scale=trunc(min(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/64)*64:trunc(min(max(iw/dar\\,ih)\\,min({0}/dar\\,{1}))/2)*2", + maxWidthParam, + maxHeightParam)); } else { - filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/2)*2:trunc(min(max(iw/dar\\,ih)\\,min({0}/dar\\,{1}))/2)*2", maxWidthParam, maxHeightParam)); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "scale=trunc(min(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/2)*2:trunc(min(max(iw/dar\\,ih)\\,min({0}/dar\\,{1}))/2)*2", + maxWidthParam, + maxHeightParam)); } } @@ -1585,54 +2484,82 @@ namespace MediaBrowser.Controller.MediaEncoding } else { - var widthParam = requestedWidth.Value.ToString(_usCulture); + var widthParam = requestedWidth.Value.ToString(CultureInfo.InvariantCulture); - filters.Add(string.Format("scale={0}:trunc(ow/a/2)*2", widthParam)); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "scale={0}:trunc(ow/a/2)*2", + widthParam)); } } // If a fixed height was requested else if (requestedHeight.HasValue) { - var heightParam = requestedHeight.Value.ToString(_usCulture); + var heightParam = requestedHeight.Value.ToString(CultureInfo.InvariantCulture); if (isExynosV4L2) { - filters.Add(string.Format("scale=trunc(oh*a/64)*64:{0}", heightParam)); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "scale=trunc(oh*a/64)*64:{0}", + heightParam)); } else { - filters.Add(string.Format("scale=trunc(oh*a/2)*2:{0}", heightParam)); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "scale=trunc(oh*a/2)*2:{0}", + heightParam)); } } // If a max width was requested else if (requestedMaxWidth.HasValue) { - var maxWidthParam = requestedMaxWidth.Value.ToString(_usCulture); + var maxWidthParam = requestedMaxWidth.Value.ToString(CultureInfo.InvariantCulture); if (isExynosV4L2) { - filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,{0})/64)*64:trunc(ow/dar/2)*2", maxWidthParam)); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "scale=trunc(min(max(iw\\,ih*dar)\\,{0})/64)*64:trunc(ow/dar/2)*2", + maxWidthParam)); } else { - filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,{0})/2)*2:trunc(ow/dar/2)*2", maxWidthParam)); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "scale=trunc(min(max(iw\\,ih*dar)\\,{0})/2)*2:trunc(ow/dar/2)*2", + maxWidthParam)); } } // If a max height was requested else if (requestedMaxHeight.HasValue) { - var maxHeightParam = requestedMaxHeight.Value.ToString(_usCulture); + var maxHeightParam = requestedMaxHeight.Value.ToString(CultureInfo.InvariantCulture); if (isExynosV4L2) { - filters.Add(string.Format("scale=trunc(oh*a/64)*64:min(max(iw/dar\\,ih)\\,{0})", maxHeightParam)); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "scale=trunc(oh*a/64)*64:min(max(iw/dar\\,ih)\\,{0})", + maxHeightParam)); } else { - filters.Add(string.Format("scale=trunc(oh*a/2)*2:min(max(iw/dar\\,ih)\\,{0})", maxHeightParam)); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "scale=trunc(oh*a/2)*2:min(max(iw/dar\\,ih)\\,{0})", + maxHeightParam)); } } } @@ -1642,8 +2569,8 @@ namespace MediaBrowser.Controller.MediaEncoding private string GetFixedSizeScalingFilter(Video3DFormat? threedFormat, int requestedWidth, int requestedHeight) { - var widthParam = requestedWidth.ToString(_usCulture); - var heightParam = requestedHeight.ToString(_usCulture); + var widthParam = requestedWidth.ToString(CultureInfo.InvariantCulture); + var heightParam = requestedHeight.ToString(CultureInfo.InvariantCulture); string filter = null; @@ -1657,11 +2584,11 @@ namespace MediaBrowser.Controller.MediaEncoding break; case Video3DFormat.FullSideBySide: filter = "crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2"; - //fsbs crop width in half,set the display aspect,crop out any black bars we may have made the scale width to requestedWidth. + // fsbs crop width in half,set the display aspect,crop out any black bars we may have made the scale width to requestedWidth. break; case Video3DFormat.HalfTopAndBottom: filter = "crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2"; - //htab crop height in half,scale to correct size, set the display aspect,crop out any black bars we may have made the scale width to requestedWidth + // htab crop height in half,scale to correct size, set the display aspect,crop out any black bars we may have made the scale width to requestedWidth break; case Video3DFormat.FullTopAndBottom: filter = "crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2"; @@ -1685,62 +2612,490 @@ namespace MediaBrowser.Controller.MediaEncoding } } - return string.Format(filter, widthParam, heightParam); + return string.Format(CultureInfo.InvariantCulture, filter, widthParam, heightParam); } /// <summary> - /// If we're going to put a fixed size on the command line, this will calculate it + /// Gets the output size parameter. /// </summary> - public string GetOutputSizeParam(EncodingJobInfo state, + /// <param name="state">Encoding state.</param> + /// <param name="options">Encoding options.</param> + /// <param name="outputVideoCodec">Video codec to use.</param> + /// <returns>The output size parameter.</returns> + public string GetOutputSizeParam( + EncodingJobInfo state, EncodingOptions options, - string outputVideoCodec, - bool allowTimeStampCopy = true) + string outputVideoCodec) + { + string filters = GetOutputSizeParamInternal(state, options, outputVideoCodec); + return string.IsNullOrEmpty(filters) ? string.Empty : " -vf \"" + filters + "\""; + } + + /// <summary> + /// Gets the output size parameter. + /// If we're going to put a fixed size on the command line, this will calculate it. + /// </summary> + /// <param name="state">Encoding state.</param> + /// <param name="options">Encoding options.</param> + /// <param name="outputVideoCodec">Video codec to use.</param> + /// <returns>The output size parameter.</returns> + public string GetOutputSizeParamInternal( + EncodingJobInfo state, + EncodingOptions options, + string outputVideoCodec) { // http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/ var request = state.BaseRequest; - + var videoStream = state.VideoStream; var filters = new List<string>(); - if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options) ?? string.Empty; + var inputWidth = videoStream?.Width; + var inputHeight = videoStream?.Height; + var threeDFormat = state.MediaSource.Video3DFormat; + + var isSwDecoder = string.IsNullOrEmpty(videoDecoder); + var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1; + var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; + var isVaapiEncoder = outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; + var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1; + var isVaapiHevcEncoder = outputVideoCodec.IndexOf("hevc_vaapi", StringComparison.OrdinalIgnoreCase) != -1; + var isQsvH264Encoder = outputVideoCodec.IndexOf("h264_qsv", StringComparison.OrdinalIgnoreCase) != -1; + var isQsvHevcEncoder = outputVideoCodec.IndexOf("hevc_qsv", StringComparison.OrdinalIgnoreCase) != -1; + var isNvdecDecoder = videoDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase); + var isNvencEncoder = outputVideoCodec.Contains("nvenc", StringComparison.OrdinalIgnoreCase); + var isCuvidH264Decoder = videoDecoder.Contains("h264_cuvid", StringComparison.OrdinalIgnoreCase); + var isCuvidHevcDecoder = videoDecoder.Contains("hevc_cuvid", StringComparison.OrdinalIgnoreCase); + var isCuvidVp9Decoder = videoDecoder.Contains("vp9_cuvid", StringComparison.OrdinalIgnoreCase); + var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", StringComparison.OrdinalIgnoreCase) != -1; + var isLibX265Encoder = outputVideoCodec.IndexOf("libx265", StringComparison.OrdinalIgnoreCase) != -1; + var isLinux = OperatingSystem.IsLinux(); + var isColorDepth10 = IsColorDepth10(state); + + var isTonemappingSupportedOnNvenc = string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && (isNvdecDecoder || isCuvidHevcDecoder || isCuvidVp9Decoder || isSwDecoder); + var isTonemappingSupportedOnAmf = string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && (isD3d11vaDecoder || isSwDecoder); + var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder); + var isTonemappingSupportedOnQsv = string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isQsvH264Encoder || isQsvHevcEncoder); + var isOpenclTonemappingSupported = IsOpenclTonemappingSupported(state, options); + var isVppTonemappingSupported = IsVppTonemappingSupported(state, options); + var isCudaTonemappingSupported = IsCudaTonemappingSupported(state, options); + var mediaEncoderVersion = _mediaEncoder.GetMediaEncoderVersion(); + var isCudaOverlaySupported = _mediaEncoder.SupportsFilter("overlay_cuda") && mediaEncoderVersion != null && mediaEncoderVersion >= _minVersionForCudaOverlay; + + var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + + // If double rate deinterlacing is enabled and the input framerate is 30fps or below, otherwise the output framerate will be too high for many devices + var doubleRateDeinterlace = options.DeinterlaceDoubleRate && (videoStream?.AverageFrameRate ?? 60) <= 30; + + var isScalingInAdvance = false; + var isCudaDeintInAdvance = false; + var isHwuploadCudaRequired = false; + var isNoTonemapFilterApplied = true; + var isDeinterlaceH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); + var isDeinterlaceHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); + + // Add OpenCL tonemapping filter for NVENC/AMF/VAAPI. + if ((isTonemappingSupportedOnNvenc && !isCudaTonemappingSupported) || isTonemappingSupportedOnAmf || (isTonemappingSupportedOnVaapi && !isVppTonemappingSupported)) + { + // NVIDIA Pascal and Turing or higher are recommended. + // AMD Polaris and Vega or higher are recommended. + // Intel Kaby Lake or newer is required. + if (isOpenclTonemappingSupported) + { + isNoTonemapFilterApplied = false; + var inputHdrParams = GetInputHdrParams(videoStream.ColorTransfer); + if (!string.IsNullOrEmpty(inputHdrParams)) + { + filters.Add(inputHdrParams); + } + + var parameters = "tonemap_opencl=format=nv12:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:desat={1}:threshold={2}:peak={3}"; + + if (options.TonemappingParam != 0) + { + parameters += ":param={4}"; + } + + if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase)) + { + parameters += ":range={5}"; + } + + if (isSwDecoder || isD3d11vaDecoder) + { + isScalingInAdvance = true; + // Add zscale filter before tone mapping filter for performance. + var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight); + if (width.HasValue && height.HasValue) + { + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "zscale=s={0}x{1}", + width.Value, + height.Value)); + } + + // Convert to hardware pixel format p010 when using SW decoder. + filters.Add("format=p010"); + } + + if ((isDeinterlaceH264 || isDeinterlaceHevc) && isNvdecDecoder) + { + isCudaDeintInAdvance = true; + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "yadif_cuda={0}:-1:0", + doubleRateDeinterlace ? "1" : "0")); + } + + if (isVaapiDecoder || isNvdecDecoder) + { + isScalingInAdvance = true; + filters.AddRange( + GetScalingFilters( + state, + options, + inputWidth, + inputHeight, + threeDFormat, + videoDecoder, + outputVideoCodec, + request.Width, + request.Height, + request.MaxWidth, + request.MaxHeight)); + } + + // hwmap the HDR data to opencl device by cl-va p010 interop. + if (isVaapiDecoder) + { + filters.Add("hwmap"); + } + + // convert cuda device data to p010 host data. + if (isNvdecDecoder) + { + filters.Add("hwdownload,format=p010"); + } + + if (isNvdecDecoder + || isCuvidHevcDecoder + || isCuvidVp9Decoder + || isSwDecoder + || isD3d11vaDecoder) + { + // Upload the HDR10 or HLG data to the OpenCL device, + // use tonemap_opencl filter for tone mapping, + // and then download the SDR data to memory. + filters.Add("hwupload"); + } + + // Fallback to hable if bt2390 is chosen but not supported in tonemap_opencl. + var isBt2390SupportedInOpenclTonemap = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.TonemapOpenclBt2390); + if (string.Equals(options.TonemappingAlgorithm, "bt2390", StringComparison.OrdinalIgnoreCase) + && !isBt2390SupportedInOpenclTonemap) + { + options.TonemappingAlgorithm = "hable"; + } + + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + parameters, + options.TonemappingAlgorithm, + options.TonemappingDesat, + options.TonemappingThreshold, + options.TonemappingPeak, + options.TonemappingParam, + options.TonemappingRange)); + + if (isNvdecDecoder + || isCuvidHevcDecoder + || isCuvidVp9Decoder + || isSwDecoder + || isD3d11vaDecoder) + { + filters.Add("hwdownload"); + filters.Add("format=nv12"); + } + + if (isNvdecDecoder && isNvencEncoder) + { + isHwuploadCudaRequired = true; + } + + if (isVaapiDecoder) + { + // Reverse the data route from opencl to vaapi. + filters.Add("hwmap=derive_device=vaapi:reverse=1"); + } + + var outputSdrParams = GetOutputSdrParams(options.TonemappingRange); + if (!string.IsNullOrEmpty(outputSdrParams)) + { + filters.Add(outputSdrParams); + } + } + } + + // When the input may or may not be hardware VAAPI decodable. + if ((isVaapiH264Encoder || isVaapiHevcEncoder) + && !(isTonemappingSupportedOnVaapi && (isOpenclTonemappingSupported || isVppTonemappingSupported))) { filters.Add("format=nv12|vaapi"); filters.Add("hwupload"); } - if (state.DeInterlace("h264", true) && string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + // When burning in graphical subtitles using overlay_qsv, upload videostream to the same qsv context. + else if (isLinux && hasGraphicalSubs && (isQsvH264Encoder || isQsvHevcEncoder) + && !(isTonemappingSupportedOnQsv && isVppTonemappingSupported)) { - filters.Add(string.Format("deinterlace_vaapi")); + filters.Add("hwupload=extra_hw_frames=64"); } - var videoStream = state.VideoStream; + // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first. + else if ((IsVaapiSupported(state) && isVaapiDecoder) && (isLibX264Encoder || isLibX265Encoder) + && !(isTonemappingSupportedOnQsv && isVppTonemappingSupported)) + { + var codec = videoStream.Codec; + + // Assert 10-bit hardware VAAPI decodable + if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))) + { + /* + Download data from GPU to CPU as p010le format. + Colorspace conversion is unnecessary here as libx264 will handle it. + If this step is missing, it will fail on AMD but not on intel. + */ + filters.Add("hwdownload"); + filters.Add("format=p010le"); + } + + // Assert 8-bit hardware VAAPI decodable + else if (!isColorDepth10) + { + filters.Add("hwdownload"); + filters.Add("format=nv12"); + } + } - if (state.DeInterlace("h264", true) && !string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + // Add hardware deinterlace filter before scaling filter. + if (isDeinterlaceH264 || isDeinterlaceHevc) { - var inputFramerate = videoStream == null ? null : videoStream.RealFrameRate; + if (isVaapiEncoder + || (isTonemappingSupportedOnQsv && isVppTonemappingSupported)) + { + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "deinterlace_vaapi=rate={0}", + doubleRateDeinterlace ? "field" : "frame")); + } + else if (isNvdecDecoder && !isCudaDeintInAdvance) + { + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "yadif_cuda={0}:-1:0", + doubleRateDeinterlace ? "1" : "0")); + } + } - // If it is already 60fps then it will create an output framerate that is much too high for roku and others to handle - if (string.Equals(options.DeinterlaceMethod, "bobandweave", StringComparison.OrdinalIgnoreCase) && (inputFramerate ?? 60) <= 30) + // Add software deinterlace filter before scaling filter. + if ((isDeinterlaceH264 || isDeinterlaceHevc) + && !isVaapiH264Encoder + && !isVaapiHevcEncoder + && !isQsvH264Encoder + && !isQsvHevcEncoder + && !isNvdecDecoder + && !isCuvidH264Decoder) + { + if (string.Equals(options.DeinterlaceMethod, "bwdif", StringComparison.OrdinalIgnoreCase)) { - filters.Add("yadif=1:-1:0"); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "bwdif={0}:-1:0", + doubleRateDeinterlace ? "1" : "0")); } else { - filters.Add("yadif=0:-1:0"); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "yadif={0}:-1:0", + doubleRateDeinterlace ? "1" : "0")); } } - var inputWidth = videoStream == null ? null : videoStream.Width; - var inputHeight = videoStream == null ? null : videoStream.Height; - var threeDFormat = state.MediaSource.Video3DFormat; + // Add scaling filter: scale_*=format=nv12 or scale_*=w=*:h=*:format=nv12 or scale=expr + if (!isScalingInAdvance) + { + filters.AddRange( + GetScalingFilters( + state, + options, + inputWidth, + inputHeight, + threeDFormat, + videoDecoder, + outputVideoCodec, + request.Width, + request.Height, + request.MaxWidth, + request.MaxHeight)); + } - var videoDecoder = this.GetHardwareAcceleratedVideoDecoder(state, options); + // Add Cuda tonemapping filter. + if (isNvdecDecoder && isCudaTonemappingSupported) + { + isNoTonemapFilterApplied = false; + var inputHdrParams = GetInputHdrParams(videoStream.ColorTransfer); + if (!string.IsNullOrEmpty(inputHdrParams)) + { + filters.Add(inputHdrParams); + } - filters.AddRange(GetScalingFilters(inputWidth, inputHeight, threeDFormat, videoDecoder, outputVideoCodec, request.Width, request.Height, request.MaxWidth, request.MaxHeight)); + var parameters = (hasGraphicalSubs && isCudaOverlaySupported && isNvencEncoder) + ? "tonemap_cuda=format=yuv420p:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:peak={1}:desat={2}" + : "tonemap_cuda=format=nv12:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:peak={1}:desat={2}"; - var output = string.Empty; + if (options.TonemappingParam != 0) + { + parameters += ":param={3}"; + } - if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) + if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase)) + { + parameters += ":range={4}"; + } + + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + parameters, + options.TonemappingAlgorithm, + options.TonemappingPeak, + options.TonemappingDesat, + options.TonemappingParam, + options.TonemappingRange)); + + if (isLibX264Encoder + || isLibX265Encoder + || hasTextSubs + || (hasGraphicalSubs && !isCudaOverlaySupported && isNvencEncoder)) + { + if (isNvencEncoder) + { + isHwuploadCudaRequired = true; + } + + filters.Add("hwdownload"); + filters.Add("format=nv12"); + } + + var outputSdrParams = GetOutputSdrParams(options.TonemappingRange); + if (!string.IsNullOrEmpty(outputSdrParams)) + { + filters.Add(outputSdrParams); + } + } + + // Add VPP tonemapping filter for VAAPI. + // Full hardware based video post processing, faster than OpenCL but lacks fine tuning options. + if ((isTonemappingSupportedOnVaapi || isTonemappingSupportedOnQsv) + && isVppTonemappingSupported) + { + filters.Add("tonemap_vaapi=format=nv12:transfer=bt709:matrix=bt709:primaries=bt709"); + } + + // Another case is when using Nvenc decoder. + if (isNvdecDecoder && !isOpenclTonemappingSupported && !isCudaTonemappingSupported) + { + var codec = videoStream.Codec; + var isCudaFormatConversionSupported = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.ScaleCudaFormat); + + // Assert 10-bit hardware decodable + if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))) + { + if (isCudaFormatConversionSupported) + { + if (isLibX264Encoder + || isLibX265Encoder + || hasTextSubs + || (hasGraphicalSubs && !isCudaOverlaySupported && isNvencEncoder)) + { + if (isNvencEncoder) + { + isHwuploadCudaRequired = true; + } + + filters.Add("hwdownload"); + filters.Add("format=nv12"); + } + } + else + { + // Download data from GPU to CPU as p010 format. + filters.Add("hwdownload"); + filters.Add("format=p010"); + + // Cuda lacks of a pixel format converter. + if (isNvencEncoder) + { + isHwuploadCudaRequired = true; + filters.Add("format=yuv420p"); + } + } + } + + // Assert 8-bit hardware decodable + else if (!isColorDepth10 + && (isLibX264Encoder + || isLibX265Encoder + || hasTextSubs + || (hasGraphicalSubs && !isCudaOverlaySupported && isNvencEncoder))) + { + if (isNvencEncoder) + { + isHwuploadCudaRequired = true; + } + + filters.Add("hwdownload"); + filters.Add("format=nv12"); + } + } + + // Add parameters to use VAAPI with burn-in text subtitles (GH issue #642) + if (isVaapiH264Encoder + || isVaapiHevcEncoder + || (isTonemappingSupportedOnQsv && isVppTonemappingSupported)) + { + if (hasTextSubs) + { + // Convert hw context from ocl to va. + // For tonemapping and text subs burn-in. + if (isTonemappingSupportedOnVaapi && isOpenclTonemappingSupported && !isVppTonemappingSupported) + { + filters.Add("scale_vaapi"); + } + + // Test passed on Intel and AMD gfx + filters.Add("hwmap=mode=read+write"); + filters.Add("format=nv12"); + } + } + + if (hasTextSubs) { var subParam = GetTextSubtitleParam(state); @@ -1748,49 +3103,116 @@ namespace MediaBrowser.Controller.MediaEncoding // Ensure proper filters are passed to ffmpeg in case of hardware acceleration via VA-API // Reference: https://trac.ffmpeg.org/wiki/Hardware/VAAPI - if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + if (isVaapiH264Encoder || isVaapiHevcEncoder) { filters.Add("hwmap"); } - if (allowTimeStampCopy) + + if (isTonemappingSupportedOnQsv && isVppTonemappingSupported) + { + filters.Add("hwmap,format=vaapi"); + } + + if (isNvdecDecoder && isNvencEncoder) { - output += " -copyts"; + isHwuploadCudaRequired = true; } } + // Interop the VAAPI data to QSV for hybrid tonemapping + if (isTonemappingSupportedOnQsv && isVppTonemappingSupported && !hasGraphicalSubs) + { + filters.Add("hwmap=derive_device=qsv,scale_qsv"); + } + + if (isHwuploadCudaRequired && !hasGraphicalSubs) + { + filters.Add("hwupload_cuda"); + } + + // If no tonemap filter is applied, + // tag the video range as SDR to prevent the encoder from encoding HDR video. + if (isNoTonemapFilterApplied) + { + var outputSdrParams = GetOutputSdrParams(null); + if (!string.IsNullOrEmpty(outputSdrParams)) + { + filters.Add(outputSdrParams); + } + } + + var output = string.Empty; if (filters.Count > 0) { - output += string.Format(" -vf \"{0}\"", string.Join(",", filters.ToArray())); + output += string.Format( + CultureInfo.InvariantCulture, + "{0}", + string.Join(',', filters)); } return output; } + public static string GetInputHdrParams(string colorTransfer) + { + if (string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase)) + { + // HLG + return "setparams=color_primaries=bt2020:color_trc=arib-std-b67:colorspace=bt2020nc"; + } + else + { + // HDR10 + return "setparams=color_primaries=bt2020:color_trc=smpte2084:colorspace=bt2020nc"; + } + } + + public static string GetOutputSdrParams(string tonemappingRange) + { + // SDR + if (string.Equals(tonemappingRange, "tv", StringComparison.OrdinalIgnoreCase)) + { + return "setparams=color_primaries=bt709:color_trc=bt709:colorspace=bt709:range=tv"; + } + + if (string.Equals(tonemappingRange, "pc", StringComparison.OrdinalIgnoreCase)) + { + return "setparams=color_primaries=bt709:color_trc=bt709:colorspace=bt709:range=pc"; + } + + return "setparams=color_primaries=bt709:color_trc=bt709:colorspace=bt709"; + } /// <summary> /// Gets the number of threads. /// </summary> - public int GetNumberOfThreads(EncodingJobInfo state, EncodingOptions encodingOptions, string outputVideoCodec) + /// <param name="state">Encoding state.</param> + /// <param name="encodingOptions">Encoding options.</param> + /// <param name="outputVideoCodec">Video codec to use.</param> + /// <returns>Number of threads.</returns> +#nullable enable + public static int GetNumberOfThreads(EncodingJobInfo? state, EncodingOptions encodingOptions, string? outputVideoCodec) { - if (string.Equals(outputVideoCodec, "libvpx", StringComparison.OrdinalIgnoreCase)) - { - // per docs: - // -threads number of threads to use for encoding, can't be 0 [auto] with VP8 - // (recommended value : number of real cores - 1) - return Math.Max(Environment.ProcessorCount - 1, 1); - } + // VP8 and VP9 encoders must have their thread counts set. + bool mustSetThreadCount = string.Equals(outputVideoCodec, "libvpx", StringComparison.OrdinalIgnoreCase) + || string.Equals(outputVideoCodec, "libvpx-vp9", StringComparison.OrdinalIgnoreCase); - var threads = state.BaseRequest.CpuCoreLimit ?? encodingOptions.EncodingThreadCount; + var threads = state?.BaseRequest.CpuCoreLimit ?? encodingOptions.EncodingThreadCount; - // Automatic - if (threads <= 0 || threads >= Environment.ProcessorCount) + if (threads <= 0) { - return 0; + // Automatically set thread count + return mustSetThreadCount ? Math.Max(Environment.ProcessorCount - 1, 1) : 0; + } + else if (threads >= Environment.ProcessorCount) + { + return Environment.ProcessorCount; } return threads; } +#nullable disable public void TryStreamCopy(EncodingJobInfo state) { if (state.VideoStream != null && CanStreamCopyVideo(state, state.VideoStream)) @@ -1802,13 +3224,14 @@ namespace MediaBrowser.Controller.MediaEncoding var user = state.User; // If the user doesn't have access to transcoding, then force stream copy, regardless of whether it will be compatible or not - if (user != null && !user.Policy.EnableVideoPlaybackTranscoding) + if (user != null && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)) { state.OutputVideoCodec = "copy"; } } - if (state.AudioStream != null && CanStreamCopyAudio(state, state.AudioStream, state.SupportedAudioCodecs)) + if (state.AudioStream != null + && CanStreamCopyAudio(state, state.AudioStream, state.SupportedAudioCodecs)) { state.OutputAudioCodec = "copy"; } @@ -1817,29 +3240,17 @@ namespace MediaBrowser.Controller.MediaEncoding var user = state.User; // If the user doesn't have access to transcoding, then force stream copy, regardless of whether it will be compatible or not - if (user != null && !user.Policy.EnableAudioPlaybackTranscoding) + if (user != null && !user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) { state.OutputAudioCodec = "copy"; } } } - public static string GetProbeSizeArgument(int numInputFiles) - { - return numInputFiles > 1 ? "-probesize 1G" : ""; - } - - public static string GetAnalyzeDurationArgument(int numInputFiles) - { - return numInputFiles > 1 ? "-analyzeduration 200M" : ""; - } - public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions) { var inputModifier = string.Empty; - - var numInputFiles = state.PlayableStreamFileNames.Length > 0 ? state.PlayableStreamFileNames.Length : 1; - var probeSizeArgument = GetProbeSizeArgument(numInputFiles); + var probeSizeArgument = string.Empty; string analyzeDurationArgument; if (state.MediaSource.AnalyzeDurationMs.HasValue) @@ -1848,7 +3259,7 @@ namespace MediaBrowser.Controller.MediaEncoding } else { - analyzeDurationArgument = GetAnalyzeDurationArgument(numInputFiles); + analyzeDurationArgument = string.Empty; } if (!string.IsNullOrEmpty(probeSizeArgument)) @@ -1900,18 +3311,22 @@ namespace MediaBrowser.Controller.MediaEncoding { flags.Add("+igndts"); } + if (state.IgnoreInputIndex) { flags.Add("+ignidx"); } - if (state.GenPtsInput) + + if (state.GenPtsInput || IsCopyCodec(state.OutputVideoCodec)) { flags.Add("+genpts"); } + if (state.DiscardCorruptFramesInput) { flags.Add("+discardcorrupt"); } + if (state.EnableFastSeekInput) { flags.Add("+fastseek"); @@ -1919,24 +3334,48 @@ namespace MediaBrowser.Controller.MediaEncoding if (flags.Count > 0) { - inputModifier += " -fflags " + string.Join("", flags.ToArray()); + inputModifier += " -fflags " + string.Join(string.Empty, flags); } - var videoDecoder = this.GetHardwareAcceleratedVideoDecoder(state, encodingOptions); + var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions); + if (!string.IsNullOrEmpty(videoDecoder)) { inputModifier += " " + videoDecoder; - var videoStream = state.VideoStream; - var inputWidth = videoStream == null ? null : videoStream.Width; - var inputHeight = videoStream == null ? null : videoStream.Height; - var request = state.BaseRequest; + if (!IsCopyCodec(state.OutputVideoCodec) + && videoDecoder.Contains("cuvid", StringComparison.OrdinalIgnoreCase)) + { + var videoStream = state.VideoStream; + var inputWidth = videoStream?.Width; + var inputHeight = videoStream?.Height; + var request = state.BaseRequest; - var fixedOutputSize = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight); + var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight); - if ((videoDecoder ?? string.Empty).IndexOf("_cuvid", StringComparison.OrdinalIgnoreCase) != -1 && fixedOutputSize.Item1.HasValue && fixedOutputSize.Item2.HasValue) - { - inputModifier += string.Format(" -resize {0}x{1}", fixedOutputSize.Item1.Value.ToString(_usCulture), fixedOutputSize.Item2.Value.ToString(_usCulture)); + if (videoDecoder.Contains("cuvid", StringComparison.OrdinalIgnoreCase) + && width.HasValue + && height.HasValue) + { + if (width.HasValue && height.HasValue) + { + inputModifier += string.Format( + CultureInfo.InvariantCulture, + " -resize {0}x{1}", + width.Value, + height.Value); + } + + if (state.DeInterlace("h264", true)) + { + inputModifier += " -deint 1"; + + if (!encodingOptions.DeinterlaceDoubleRate || (videoStream?.AverageFrameRate ?? 60) > 30) + { + inputModifier += " -drop_second_field 1"; + } + } + } } } @@ -1945,9 +3384,10 @@ namespace MediaBrowser.Controller.MediaEncoding var outputVideoCodec = GetVideoEncoder(state, encodingOptions); // Important: If this is ever re-enabled, make sure not to use it with wtv because it breaks seeking - if (!string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase) && - state.TranscodingType != TranscodingJobType.Progressive && - state.EnableBreakOnNonKeyFrames(outputVideoCodec)) + if (!string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase) + && state.TranscodingType != TranscodingJobType.Progressive + && !state.EnableBreakOnNonKeyFrames(outputVideoCodec) + && (state.BaseRequest.StartTimeTicks ?? 0) > 0) { inputModifier += " -noaccurate_seek"; } @@ -1970,15 +3410,17 @@ namespace MediaBrowser.Controller.MediaEncoding return inputModifier; } - - public void AttachMediaSourceInfo(EncodingJobInfo state, - MediaSourceInfo mediaSource, - string requestedUrl) + public void AttachMediaSourceInfo( + EncodingJobInfo state, + EncodingOptions encodingOptions, + MediaSourceInfo mediaSource, + string requestedUrl) { if (state == null) { throw new ArgumentNullException(nameof(state)); } + if (mediaSource == null) { throw new ArgumentNullException(nameof(mediaSource)); @@ -2001,32 +3443,6 @@ namespace MediaBrowser.Controller.MediaEncoding state.IsoType = mediaSource.IsoType; - if (mediaSource.VideoType.HasValue) - { - state.VideoType = mediaSource.VideoType.Value; - - if (mediaSource.VideoType.Value == VideoType.BluRay || mediaSource.VideoType.Value == VideoType.Dvd) - { - state.PlayableStreamFileNames = Video.QueryPlayableStreamFiles(state.MediaPath, mediaSource.VideoType.Value).Select(Path.GetFileName).ToArray(); - } - else if (mediaSource.VideoType.Value == VideoType.Iso && state.IsoType == IsoType.BluRay) - { - state.PlayableStreamFileNames = Video.QueryPlayableStreamFiles(state.MediaPath, VideoType.BluRay).Select(Path.GetFileName).ToArray(); - } - else if (mediaSource.VideoType.Value == VideoType.Iso && state.IsoType == IsoType.Dvd) - { - state.PlayableStreamFileNames = Video.QueryPlayableStreamFiles(state.MediaPath, VideoType.Dvd).Select(Path.GetFileName).ToArray(); - } - else - { - state.PlayableStreamFileNames = Array.Empty<string>(); - } - } - else - { - state.PlayableStreamFileNames = Array.Empty<string>(); - } - if (mediaSource.Timestamp.HasValue) { state.InputTimestamp = mediaSource.Timestamp.Value; @@ -2036,15 +3452,16 @@ namespace MediaBrowser.Controller.MediaEncoding state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders; state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate; - if (state.ReadInputAtNativeFramerate || - mediaSource.Protocol == MediaProtocol.File && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase)) + if (state.ReadInputAtNativeFramerate + || (mediaSource.Protocol == MediaProtocol.File + && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase))) { state.InputVideoSync = "-1"; state.InputAudioSync = "1"; } - if (string.Equals(mediaSource.Container, "wma", StringComparison.OrdinalIgnoreCase) || - string.Equals(mediaSource.Container, "asf", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(mediaSource.Container, "wma", StringComparison.OrdinalIgnoreCase) + || string.Equals(mediaSource.Container, "asf", StringComparison.OrdinalIgnoreCase)) { // Seeing some stuttering when transcoding wma to audio-only HLS state.InputAudioSync = "1"; @@ -2088,9 +3505,10 @@ namespace MediaBrowser.Controller.MediaEncoding state.MediaSource = mediaSource; var request = state.BaseRequest; - if (!string.IsNullOrWhiteSpace(request.AudioCodec)) + var supportedAudioCodecs = state.SupportedAudioCodecs; + if (request != null && supportedAudioCodecs != null && supportedAudioCodecs.Length > 0) { - var supportedAudioCodecsList = request.AudioCodec.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); + var supportedAudioCodecsList = supportedAudioCodecs.ToList(); ShiftAudioCodecsIfNeeded(supportedAudioCodecsList, state.AudioStream); @@ -2099,11 +3517,23 @@ namespace MediaBrowser.Controller.MediaEncoding request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => _mediaEncoder.CanEncodeToAudioCodec(i)) ?? state.SupportedAudioCodecs.FirstOrDefault(); } + + var supportedVideoCodecs = state.SupportedVideoCodecs; + if (request != null && supportedVideoCodecs != null && supportedVideoCodecs.Length > 0) + { + var supportedVideoCodecsList = supportedVideoCodecs.ToList(); + + ShiftVideoCodecsIfNeeded(supportedVideoCodecsList, encodingOptions); + + state.SupportedVideoCodecs = supportedVideoCodecsList.ToArray(); + + request.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault(); + } } private void ShiftAudioCodecsIfNeeded(List<string> audioCodecs, MediaStream audioStream) { - // Nothing to do here + // No need to shift if there is only one supported audio codec. if (audioCodecs.Count < 2) { return; @@ -2131,6 +3561,34 @@ namespace MediaBrowser.Controller.MediaEncoding } } + private void ShiftVideoCodecsIfNeeded(List<string> videoCodecs, EncodingOptions encodingOptions) + { + // Shift hevc/h265 to the end of list if hevc encoding is not allowed. + if (encodingOptions.AllowHevcEncoding) + { + return; + } + + // No need to shift if there is only one supported video codec. + if (videoCodecs.Count < 2) + { + return; + } + + var shiftVideoCodecs = new[] { "hevc", "h265" }; + if (videoCodecs.All(i => shiftVideoCodecs.Contains(i, StringComparer.OrdinalIgnoreCase))) + { + return; + } + + while (shiftVideoCodecs.Contains(videoCodecs[0], StringComparer.OrdinalIgnoreCase)) + { + var removed = shiftVideoCodecs[0]; + videoCodecs.RemoveAt(0); + videoCodecs.Add(removed); + } + } + private void NormalizeSubtitleEmbed(EncodingJobInfo state) { if (state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Embed) @@ -2147,209 +3605,321 @@ namespace MediaBrowser.Controller.MediaEncoding } /// <summary> - /// Gets the name of the output video codec + /// Gets the ffmpeg option string for the hardware accelerated video decoder. /// </summary> + /// <param name="state">The encoding job info.</param> + /// <param name="encodingOptions">The encoding options.</param> + /// <returns>The option string or null if none available.</returns> protected string GetHardwareAcceleratedVideoDecoder(EncodingJobInfo state, EncodingOptions encodingOptions) { - if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + var videoStream = state.VideoStream; + + if (videoStream == null) { return null; } - return this.GetHardwareAcceleratedVideoDecoder(state.MediaSource.VideoType ?? VideoType.VideoFile, state.VideoStream, encodingOptions); - } - - public string GetHardwareAcceleratedVideoDecoder(VideoType videoType, MediaStream videoStream, EncodingOptions encodingOptions) - { + var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile; // Only use alternative encoders for video files. // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully - // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this. + // Since transcoding of folder rips is experimental anyway, it's not worth adding additional variables such as this. if (videoType != VideoType.VideoFile) { return null; } - if (videoStream != null && - !string.IsNullOrEmpty(videoStream.Codec) && - !string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType)) + if (IsCopyCodec(state.OutputVideoCodec)) + { + return null; + } + + if (!string.IsNullOrEmpty(videoStream.Codec) && !string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType)) { + var isColorDepth10 = IsColorDepth10(state); + + // Only hevc and vp9 formats have 10-bit hardware decoder support now. + if (isColorDepth10 && !(string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.Codec, "vp9", StringComparison.OrdinalIgnoreCase))) + { + return null; + } + + // Hybrid VPP tonemapping with VAAPI + if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) + && IsVppTonemappingSupported(state, encodingOptions)) + { + var outputVideoCodec = GetVideoEncoder(state, encodingOptions) ?? string.Empty; + var isQsvEncoder = outputVideoCodec.Contains("qsv", StringComparison.OrdinalIgnoreCase); + if (isQsvEncoder) + { + // Since tonemap_vaapi only support HEVC for now, no need to check the codec again. + return GetHwaccelType(state, encodingOptions, "hevc", isColorDepth10); + } + } + if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) { switch (videoStream.Codec.ToLowerInvariant()) { case "avc": case "h264": - if (_mediaEncoder.SupportsDecoder("h264_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) - { - // qsv decoder does not support 10-bit input - if ((videoStream.BitDepth ?? 8) > 8) - { - encodingOptions.HardwareDecodingCodecs = Array.Empty<string>(); - return null; - } - return "-c:v h264_qsv "; - } - break; + return GetHwDecoderName(encodingOptions, "h264_qsv", "h264", isColorDepth10); case "hevc": case "h265": - if (_mediaEncoder.SupportsDecoder("hevc_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase)) - { - //return "-c:v hevc_qsv -load_plugin hevc_hw "; - return "-c:v hevc_qsv "; - } - break; + return GetHwDecoderName(encodingOptions, "hevc_qsv", "hevc", isColorDepth10); case "mpeg2video": - if (_mediaEncoder.SupportsDecoder("mpeg2_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v mpeg2_qsv "; - } - break; + return GetHwDecoderName(encodingOptions, "mpeg2_qsv", "mpeg2video", isColorDepth10); case "vc1": - if (_mediaEncoder.SupportsDecoder("vc1_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v vc1_qsv "; - } - break; + return GetHwDecoderName(encodingOptions, "vc1_qsv", "vc1", isColorDepth10); + case "vp8": + return GetHwDecoderName(encodingOptions, "vp8_qsv", "vp8", isColorDepth10); + case "vp9": + return GetHwDecoderName(encodingOptions, "vp9_qsv", "vp9", isColorDepth10); } } - else if (string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) { switch (videoStream.Codec.ToLowerInvariant()) { case "avc": case "h264": - if (_mediaEncoder.SupportsDecoder("h264_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v h264_cuvid "; - } - break; + return encodingOptions.EnableEnhancedNvdecDecoder && IsCudaSupported() + ? GetHwaccelType(state, encodingOptions, "h264", isColorDepth10) + : GetHwDecoderName(encodingOptions, "h264_cuvid", "h264", isColorDepth10); case "hevc": case "h265": - if (_mediaEncoder.SupportsDecoder("hevc_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v hevc_cuvid "; - } - break; + return encodingOptions.EnableEnhancedNvdecDecoder && IsCudaSupported() + ? GetHwaccelType(state, encodingOptions, "hevc", isColorDepth10) + : GetHwDecoderName(encodingOptions, "hevc_cuvid", "hevc", isColorDepth10); case "mpeg2video": - if (_mediaEncoder.SupportsDecoder("mpeg2_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v mpeg2_cuvid "; - } - break; + return encodingOptions.EnableEnhancedNvdecDecoder && IsCudaSupported() + ? GetHwaccelType(state, encodingOptions, "mpeg2video", isColorDepth10) + : GetHwDecoderName(encodingOptions, "mpeg2_cuvid", "mpeg2video", isColorDepth10); case "vc1": - if (_mediaEncoder.SupportsDecoder("vc1_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v vc1_cuvid "; - } - break; + return encodingOptions.EnableEnhancedNvdecDecoder && IsCudaSupported() + ? GetHwaccelType(state, encodingOptions, "vc1", isColorDepth10) + : GetHwDecoderName(encodingOptions, "vc1_cuvid", "vc1", isColorDepth10); case "mpeg4": - if (_mediaEncoder.SupportsDecoder("mpeg4_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v mpeg4_cuvid "; - } - break; + return encodingOptions.EnableEnhancedNvdecDecoder && IsCudaSupported() + ? GetHwaccelType(state, encodingOptions, "mpeg4", isColorDepth10) + : GetHwDecoderName(encodingOptions, "mpeg4_cuvid", "mpeg4", isColorDepth10); + case "vp8": + return encodingOptions.EnableEnhancedNvdecDecoder && IsCudaSupported() + ? GetHwaccelType(state, encodingOptions, "vp8", isColorDepth10) + : GetHwDecoderName(encodingOptions, "vp8_cuvid", "vp8", isColorDepth10); + case "vp9": + return encodingOptions.EnableEnhancedNvdecDecoder && IsCudaSupported() + ? GetHwaccelType(state, encodingOptions, "vp9", isColorDepth10) + : GetHwDecoderName(encodingOptions, "vp9_cuvid", "vp9", isColorDepth10); } } - else if (string.Equals(encodingOptions.HardwareAccelerationType, "mediacodec", StringComparison.OrdinalIgnoreCase)) { switch (videoStream.Codec.ToLowerInvariant()) { case "avc": case "h264": - if (_mediaEncoder.SupportsDecoder("h264_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v h264_mediacodec "; - } - break; + return GetHwDecoderName(encodingOptions, "h264_mediacodec", "h264", isColorDepth10); case "hevc": case "h265": - if (_mediaEncoder.SupportsDecoder("hevc_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v hevc_mediacodec "; - } - break; + return GetHwDecoderName(encodingOptions, "hevc_mediacodec", "hevc", isColorDepth10); case "mpeg2video": - if (_mediaEncoder.SupportsDecoder("mpeg2_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v mpeg2_mediacodec "; - } - break; + return GetHwDecoderName(encodingOptions, "mpeg2_mediacodec", "mpeg2video", isColorDepth10); case "mpeg4": - if (_mediaEncoder.SupportsDecoder("mpeg4_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v mpeg4_mediacodec "; - } - break; + return GetHwDecoderName(encodingOptions, "mpeg4_mediacodec", "mpeg4", isColorDepth10); case "vp8": - if (_mediaEncoder.SupportsDecoder("vp8_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("vp8", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v vp8_mediacodec "; - } - break; + return GetHwDecoderName(encodingOptions, "vp8_mediacodec", "vp8", isColorDepth10); case "vp9": - if (_mediaEncoder.SupportsDecoder("vp9_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("vp9", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v vp9_mediacodec "; - } - break; + return GetHwDecoderName(encodingOptions, "vp9_mediacodec", "vp9", isColorDepth10); } } - else if (string.Equals(encodingOptions.HardwareAccelerationType, "omx", StringComparison.OrdinalIgnoreCase)) { switch (videoStream.Codec.ToLowerInvariant()) { case "avc": case "h264": - if (_mediaEncoder.SupportsDecoder("h264_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v h264_mmal"; - } - break; + return GetHwDecoderName(encodingOptions, "h264_mmal", "h264", isColorDepth10); case "mpeg2video": - if (_mediaEncoder.SupportsDecoder("mpeg2_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v mpeg2_mmal"; - } - break; + return GetHwDecoderName(encodingOptions, "mpeg2_mmal", "mpeg2video", isColorDepth10); + case "mpeg4": + return GetHwDecoderName(encodingOptions, "mpeg4_mmal", "mpeg4", isColorDepth10); + case "vc1": + return GetHwDecoderName(encodingOptions, "vc1_mmal", "vc1", isColorDepth10); } } - else if (string.Equals(encodingOptions.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)) { - if (Environment.OSVersion.Platform == PlatformID.Win32NT) + switch (videoStream.Codec.ToLowerInvariant()) { - return "-hwaccel dxva2"; + case "avc": + case "h264": + return GetHwaccelType(state, encodingOptions, "h264", isColorDepth10); + case "hevc": + case "h265": + return GetHwaccelType(state, encodingOptions, "hevc", isColorDepth10); + case "mpeg2video": + return GetHwaccelType(state, encodingOptions, "mpeg2video", isColorDepth10); + case "vc1": + return GetHwaccelType(state, encodingOptions, "vc1", isColorDepth10); + case "mpeg4": + return GetHwaccelType(state, encodingOptions, "mpeg4", isColorDepth10); + case "vp9": + return GetHwaccelType(state, encodingOptions, "vp9", isColorDepth10); } - + } + else if (string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) + { switch (videoStream.Codec.ToLowerInvariant()) { case "avc": case "h264": - if (_mediaEncoder.SupportsDecoder("h264_amf") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v h264_amf"; - } - break; + return GetHwaccelType(state, encodingOptions, "h264", isColorDepth10); + case "hevc": + case "h265": + return GetHwaccelType(state, encodingOptions, "hevc", isColorDepth10); case "mpeg2video": - if (_mediaEncoder.SupportsDecoder("hevc_amf") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v mpeg2_mmal"; - } - break; + return GetHwaccelType(state, encodingOptions, "mpeg2video", isColorDepth10); + case "vc1": + return GetHwaccelType(state, encodingOptions, "vc1", isColorDepth10); + case "vp8": + return GetHwaccelType(state, encodingOptions, "vp8", isColorDepth10); + case "vp9": + return GetHwaccelType(state, encodingOptions, "vp9", isColorDepth10); + } + } + else if (string.Equals(encodingOptions.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) + { + switch (videoStream.Codec.ToLowerInvariant()) + { + case "avc": + case "h264": + return GetHwDecoderName(encodingOptions, "h264_opencl", "h264", isColorDepth10); + case "hevc": + case "h265": + return GetHwDecoderName(encodingOptions, "hevc_opencl", "hevc", isColorDepth10); + case "mpeg2video": + return GetHwDecoderName(encodingOptions, "mpeg2_opencl", "mpeg2video", isColorDepth10); + case "mpeg4": + return GetHwDecoderName(encodingOptions, "mpeg4_opencl", "mpeg4", isColorDepth10); + case "vc1": + return GetHwDecoderName(encodingOptions, "vc1_opencl", "vc1", isColorDepth10); + case "vp8": + return GetHwDecoderName(encodingOptions, "vp8_opencl", "vp8", isColorDepth10); + case "vp9": + return GetHwDecoderName(encodingOptions, "vp9_opencl", "vp9", isColorDepth10); } } } + var whichCodec = videoStream.Codec?.ToLowerInvariant(); + switch (whichCodec) + { + case "avc": + whichCodec = "h264"; + break; + case "h265": + whichCodec = "hevc"; + break; + } + // Avoid a second attempt if no hardware acceleration is being used - encodingOptions.HardwareDecodingCodecs = Array.Empty<string>(); + encodingOptions.HardwareDecodingCodecs = encodingOptions.HardwareDecodingCodecs.Where(val => val != whichCodec).ToArray(); // leave blank so ffmpeg will decide return null; } + /// <summary> + /// Gets a hw decoder name. + /// </summary> + /// <param name="options">Encoding options.</param> + /// <param name="decoder">Decoder to use.</param> + /// <param name="videoCodec">Video codec to use.</param> + /// <param name="isColorDepth10">Specifies if color depth 10.</param> + /// <returns>Hardware decoder name.</returns> + public string GetHwDecoderName(EncodingOptions options, string decoder, string videoCodec, bool isColorDepth10) + { + var isCodecAvailable = _mediaEncoder.SupportsDecoder(decoder) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase); + if (isColorDepth10 && isCodecAvailable) + { + if ((options.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase) && !options.EnableDecodingColorDepth10Hevc) + || (options.HardwareDecodingCodecs.Contains("vp9", StringComparer.OrdinalIgnoreCase) && !options.EnableDecodingColorDepth10Vp9)) + { + return null; + } + } + + return isCodecAvailable ? ("-c:v " + decoder) : null; + } + + /// <summary> + /// Gets a hwaccel type to use as a hardware decoder(dxva/vaapi) depending on the system. + /// </summary> + /// <param name="state">Encoding state.</param> + /// <param name="options">Encoding options.</param> + /// <param name="videoCodec">Video codec to use.</param> + /// <param name="isColorDepth10">Specifies if color depth 10.</param> + /// <returns>Hardware accelerator type.</returns> + public string GetHwaccelType(EncodingJobInfo state, EncodingOptions options, string videoCodec, bool isColorDepth10) + { + var isWindows = OperatingSystem.IsWindows(); + var isLinux = OperatingSystem.IsLinux(); + var isWindows8orLater = Environment.OSVersion.Version.Major > 6 || (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor > 1); + var isDxvaSupported = _mediaEncoder.SupportsHwaccel("dxva2") || _mediaEncoder.SupportsHwaccel("d3d11va"); + var isCodecAvailable = options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase); + + if (isColorDepth10 && isCodecAvailable) + { + if ((options.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase) && !options.EnableDecodingColorDepth10Hevc) + || (options.HardwareDecodingCodecs.Contains("vp9", StringComparer.OrdinalIgnoreCase) && !options.EnableDecodingColorDepth10Vp9)) + { + return null; + } + } + + if (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)) + { + // Currently there is no AMF decoder on Linux, only have h264 encoder. + if (isDxvaSupported && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase)) + { + if (isWindows && isWindows8orLater) + { + return "-hwaccel d3d11va"; + } + + if (isWindows && !isWindows8orLater) + { + return "-hwaccel dxva2"; + } + } + } + + if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) + || (string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) + && IsVppTonemappingSupported(state, options))) + { + if (IsVaapiSupported(state) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase)) + { + if (isLinux) + { + return "-hwaccel vaapi"; + } + } + } + + if (string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) + { + if (options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase)) + { + return "-hwaccel cuda"; + } + } + + return null; + } + public string GetSubtitleEmbedArguments(EncodingJobInfo state) { if (state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Embed) @@ -2369,14 +3939,10 @@ namespace MediaBrowser.Controller.MediaEncoding codec = format; } - var args = " -codec:s:0 " + codec; - - args += " -disposition:s:0 default"; - - return args; + return " -codec:s:0 " + codec + " -disposition:s:0 default"; } - public string GetProgressiveVideoFullCommandLine(EncodingJobInfo state, EncodingOptions encodingOptions, string outputPath, string defaultH264Preset) + public string GetProgressiveVideoFullCommandLine(EncodingJobInfo state, EncodingOptions encodingOptions, string outputPath, string defaultPreset) { // Get the output codec name var videoCodec = GetVideoEncoder(state, encodingOptions); @@ -2384,8 +3950,8 @@ namespace MediaBrowser.Controller.MediaEncoding var format = string.Empty; var keyFrame = string.Empty; - if (string.Equals(Path.GetExtension(outputPath), ".mp4", StringComparison.OrdinalIgnoreCase) && - state.BaseRequest.Context == EncodingContext.Streaming) + if (string.Equals(Path.GetExtension(outputPath), ".mp4", StringComparison.OrdinalIgnoreCase) + && state.BaseRequest.Context == EncodingContext.Streaming) { // Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js format = " -f mp4 -movflags frag_keyframe+empty_moov"; @@ -2395,18 +3961,19 @@ namespace MediaBrowser.Controller.MediaEncoding var inputModifier = GetInputModifier(state, encodingOptions); - return string.Format("{0} {1}{2} {3} {4} -map_metadata -1 -map_chapters -1 -threads {5} {6}{7}{8} -y \"{9}\"", + return string.Format( + CultureInfo.InvariantCulture, + "{0} {1}{2} {3} {4} -map_metadata -1 -map_chapters -1 -threads {5} {6}{7}{8} -y \"{9}\"", inputModifier, GetInputArgument(state, encodingOptions), keyFrame, GetMapArgs(state), - GetProgressiveVideoArguments(state, encodingOptions, videoCodec, defaultH264Preset), + GetProgressiveVideoArguments(state, encodingOptions, videoCodec, defaultPreset), threads, GetProgressiveVideoAudioArguments(state, encodingOptions), GetSubtitleEmbedArguments(state), format, - outputPath - ).Trim(); + outputPath).Trim(); } public string GetOutputFFlags(EncodingJobInfo state) @@ -2419,13 +3986,13 @@ namespace MediaBrowser.Controller.MediaEncoding if (flags.Count > 0) { - return " -fflags " + string.Join("", flags.ToArray()); + return " -fflags " + string.Join(string.Empty, flags); } return string.Empty; } - public string GetProgressiveVideoArguments(EncodingJobInfo state, EncodingOptions encodingOptions, string videoCodec, string defaultH264Preset) + public string GetProgressiveVideoArguments(EncodingJobInfo state, EncodingOptions encodingOptions, string videoCodec, string defaultPreset) { var args = "-codec:v:0 " + videoCodec; @@ -2434,13 +4001,17 @@ namespace MediaBrowser.Controller.MediaEncoding args += " -mpegts_m2ts_mode 1"; } - if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (IsCopyCodec(videoCodec)) { - if (state.VideoStream != null && IsH264(state.VideoStream) && - string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && - !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) + if (state.VideoStream != null + && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) + && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) { - args += " -bsf:v h264_mp4toannexb"; + string bitStreamArgs = GetBitStreamArgs(state.VideoStream); + if (!string.IsNullOrEmpty(bitStreamArgs)) + { + args += " " + bitStreamArgs; + } } if (state.RunTimeTicks.HasValue && state.BaseRequest.CopyTimestamps) @@ -2450,53 +4021,63 @@ namespace MediaBrowser.Controller.MediaEncoding if (!state.RunTimeTicks.HasValue) { - args += " -flags -global_header -fflags +genpts"; + args += " -fflags +genpts"; } } else { - var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"", - 5.ToString(_usCulture)); + var keyFrameArg = string.Format( + CultureInfo.InvariantCulture, + " -force_key_frames \"expr:gte(t,n_forced*{0})\"", + 5); args += keyFrameArg; var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasCopyTs = false; + // Add resolution params, if specified if (!hasGraphicalSubs) { var outputSizeParam = GetOutputSizeParam(state, encodingOptions, videoCodec); + args += outputSizeParam; + hasCopyTs = outputSizeParam.IndexOf("copyts", StringComparison.OrdinalIgnoreCase) != -1; } + // This is for graphical subs + if (hasGraphicalSubs) + { + var graphicalSubtitleParam = GetGraphicalSubtitleParam(state, encodingOptions, videoCodec); + + args += graphicalSubtitleParam; + + hasCopyTs = graphicalSubtitleParam.IndexOf("copyts", StringComparison.OrdinalIgnoreCase) != -1; + } + if (state.RunTimeTicks.HasValue && state.BaseRequest.CopyTimestamps) { if (!hasCopyTs) { args += " -copyts"; } - args += " -avoid_negative_ts disabled -start_at_zero"; - } - // This is for internal graphical subs - if (hasGraphicalSubs) - { - args += GetGraphicalSubtitleParam(state, encodingOptions, videoCodec); + args += " -avoid_negative_ts disabled"; + + if (!(state.SubtitleStream != null && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)) + { + args += " -start_at_zero"; + } } - var qualityParam = GetVideoQualityParam(state, videoCodec, encodingOptions, defaultH264Preset); + var qualityParam = GetVideoQualityParam(state, videoCodec, encodingOptions, defaultPreset); if (!string.IsNullOrEmpty(qualityParam)) { args += " " + qualityParam.Trim(); } - - if (!state.RunTimeTicks.HasValue) - { - args += " -flags -global_header"; - } } if (!string.IsNullOrEmpty(state.OutputVideoSync)) @@ -2522,7 +4103,7 @@ namespace MediaBrowser.Controller.MediaEncoding var args = "-codec:a:0 " + codec; - if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase)) + if (IsCopyCodec(codec)) { return args; } @@ -2539,15 +4120,15 @@ namespace MediaBrowser.Controller.MediaEncoding if (bitrate.HasValue) { - args += " -ab " + bitrate.Value.ToString(_usCulture); + args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture); } if (state.OutputAudioSampleRate.HasValue) { - args += " -ar " + state.OutputAudioSampleRate.Value.ToString(_usCulture); + args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); } - args += " " + GetAudioFilterParam(state, encodingOptions, false); + args += GetAudioFilterParam(state, encodingOptions); return args; } @@ -2560,12 +4141,12 @@ namespace MediaBrowser.Controller.MediaEncoding if (bitrate.HasValue) { - audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(_usCulture)); + audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture)); } if (state.OutputAudioChannels.HasValue) { - audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(_usCulture)); + audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture)); } // opus will fail on 44100 @@ -2573,44 +4154,67 @@ namespace MediaBrowser.Controller.MediaEncoding { if (state.OutputAudioSampleRate.HasValue) { - audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(_usCulture)); + audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)); } } - var albumCoverInput = string.Empty; - var mapArgs = string.Empty; - var metadata = string.Empty; - var vn = string.Empty; - - var hasArt = !string.IsNullOrEmpty(state.AlbumCoverPath); - hasArt = false; - - if (hasArt) - { - albumCoverInput = " -i \"" + state.AlbumCoverPath + "\""; - mapArgs = " -map 0:a -map 1:v -c:1:v copy"; - metadata = " -metadata:s:v title=\"Album cover\" -metadata:s:v comment=\"Cover(Front)\""; - } - else - { - vn = " -vn"; - } - var threads = GetNumberOfThreads(state, encodingOptions, null); var inputModifier = GetInputModifier(state, encodingOptions); - return string.Format("{0} {1}{7}{8} -threads {2}{3} {4} -id3v2_version 3 -write_id3v1 1{6} -y \"{5}\"", + return string.Format( + CultureInfo.InvariantCulture, + "{0} {1}{7}{8} -threads {2}{3} {4} -id3v2_version 3 -write_id3v1 1{6} -y \"{5}\"", inputModifier, GetInputArgument(state, encodingOptions), threads, - vn, - string.Join(" ", audioTranscodeParams.ToArray()), + " -vn", + string.Join(' ', audioTranscodeParams), outputPath, - metadata, - albumCoverInput, - mapArgs).Trim(); + string.Empty, + string.Empty, + string.Empty).Trim(); } + public static bool IsCopyCodec(string codec) + { + return string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase); + } + + public static bool IsColorDepth10(EncodingJobInfo state) + { + var result = false; + var videoStream = state.VideoStream; + + if (videoStream != null) + { + if (videoStream.BitDepth.HasValue) + { + return videoStream.BitDepth.Value == 10; + } + + if (!string.IsNullOrEmpty(videoStream.PixelFormat)) + { + result = videoStream.PixelFormat.Contains("p10", StringComparison.OrdinalIgnoreCase); + if (result) + { + return true; + } + } + + if (!string.IsNullOrEmpty(videoStream.Profile)) + { + result = videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase) + || videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase) + || videoStream.Profile.Contains("Profile 2", StringComparison.OrdinalIgnoreCase); + if (result) + { + return true; + } + } + } + + return result; + } } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index 916d691b8..e92c4a08a 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -1,60 +1,39 @@ +#nullable disable + +#pragma warning disable CS1591, SA1401 + using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using MediaBrowser.Controller.Entities; +using Jellyfin.Data.Entities; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Session; using MediaBrowser.Model.Net; +using MediaBrowser.Model.Session; namespace MediaBrowser.Controller.MediaEncoding { // For now, a common base class until the API and MediaEncoding classes are unified public class EncodingJobInfo { - public MediaStream VideoStream { get; set; } - public VideoType VideoType { get; set; } - public Dictionary<string, string> RemoteHttpHeaders { get; set; } - public string OutputVideoCodec { get; set; } - public MediaProtocol InputProtocol { get; set; } - public string MediaPath { get; set; } - public bool IsInputVideo { get; set; } - public IIsoMount IsoMount { get; set; } - public string[] PlayableStreamFileNames { get; set; } - public string OutputAudioCodec { get; set; } - public int? OutputVideoBitrate { get; set; } - public MediaStream SubtitleStream { get; set; } - public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; } - public string[] SupportedSubtitleCodecs { get; set; } - - public int InternalSubtitleStreamOffset { get; set; } - public MediaSourceInfo MediaSource { get; set; } - public User User { get; set; } - - public long? RunTimeTicks { get; set; } - - public bool ReadInputAtNativeFramerate { get; set; } - - public string OutputFilePath { get; set; } + public int? OutputAudioBitrate; + public int? OutputAudioChannels; - public string MimeType { get; set; } + private TranscodeReason[] _transcodeReasons = null; - public string GetMimeType(string outputPath, bool enableStreamDefault = true) + public EncodingJobInfo(TranscodingJobType jobType) { - if (!string.IsNullOrEmpty(MimeType)) - { - return MimeType; - } - - return MimeTypes.GetMimeType(outputPath, enableStreamDefault); + TranscodingType = jobType; + RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + SupportedAudioCodecs = Array.Empty<string>(); + SupportedVideoCodecs = Array.Empty<string>(); + SupportedSubtitleCodecs = Array.Empty<string>(); } - private TranscodeReason[] _transcodeReasons = null; public TranscodeReason[] TranscodeReasons { get @@ -77,6 +56,46 @@ namespace MediaBrowser.Controller.MediaEncoding } } + public IProgress<double> Progress { get; set; } + + public MediaStream VideoStream { get; set; } + + public VideoType VideoType { get; set; } + + public Dictionary<string, string> RemoteHttpHeaders { get; set; } + + public string OutputVideoCodec { get; set; } + + public MediaProtocol InputProtocol { get; set; } + + public string MediaPath { get; set; } + + public bool IsInputVideo { get; set; } + + public string OutputAudioCodec { get; set; } + + public int? OutputVideoBitrate { get; set; } + + public MediaStream SubtitleStream { get; set; } + + public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; } + + public string[] SupportedSubtitleCodecs { get; set; } + + public int InternalSubtitleStreamOffset { get; set; } + + public MediaSourceInfo MediaSource { get; set; } + + public User User { get; set; } + + public long? RunTimeTicks { get; set; } + + public bool ReadInputAtNativeFramerate { get; set; } + + public string OutputFilePath { get; set; } + + public string MimeType { get; set; } + public bool IgnoreInputDts => MediaSource.IgnoreDts; public bool IgnoreInputIndex => MediaSource.IgnoreIndex; @@ -112,202 +131,34 @@ namespace MediaBrowser.Controller.MediaEncoding public string AlbumCoverPath { get; set; } public string InputAudioSync { get; set; } + public string InputVideoSync { get; set; } + public TransportStreamTimestamp InputTimestamp { get; set; } public MediaStream AudioStream { get; set; } + public string[] SupportedAudioCodecs { get; set; } + public string[] SupportedVideoCodecs { get; set; } + public string InputContainer { get; set; } + public IsoType? IsoType { get; set; } public BaseEncodingJobOptions BaseRequest { get; set; } - public long? StartTimeTicks => BaseRequest.StartTimeTicks; - - public bool CopyTimestamps => BaseRequest.CopyTimestamps; - - public int? OutputAudioBitrate; - public int? OutputAudioChannels; - - public bool DeInterlace(string videoCodec, bool forceDeinterlaceIfSourceIsInterlaced) - { - var videoStream = VideoStream; - var isInputInterlaced = videoStream != null && videoStream.IsInterlaced; - - if (!isInputInterlaced) - { - return false; - } - - // Support general param - if (BaseRequest.DeInterlace) - { - return true; - } - - if (!string.IsNullOrEmpty(videoCodec)) - { - if (string.Equals(BaseRequest.GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return forceDeinterlaceIfSourceIsInterlaced && isInputInterlaced; - } - - public string[] GetRequestedProfiles(string codec) - { - if (!string.IsNullOrEmpty(BaseRequest.Profile)) - { - return BaseRequest.Profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries); - } - - if (!string.IsNullOrEmpty(codec)) - { - var profile = BaseRequest.GetOption(codec, "profile"); - - if (!string.IsNullOrEmpty(profile)) - { - return profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries); - } - } - - return Array.Empty<string>(); - } - - public string GetRequestedLevel(string codec) - { - if (!string.IsNullOrEmpty(BaseRequest.Level)) - { - return BaseRequest.Level; - } - - if (!string.IsNullOrEmpty(codec)) - { - return BaseRequest.GetOption(codec, "level"); - } - - return null; - } - - public int? GetRequestedMaxRefFrames(string codec) - { - if (BaseRequest.MaxRefFrames.HasValue) - { - return BaseRequest.MaxRefFrames; - } - - if (!string.IsNullOrEmpty(codec)) - { - var value = BaseRequest.GetOption(codec, "maxrefframes"); - if (!string.IsNullOrEmpty(value) - && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) - { - return result; - } - } - - return null; - } - - public int? GetRequestedVideoBitDepth(string codec) - { - if (BaseRequest.MaxVideoBitDepth.HasValue) - { - return BaseRequest.MaxVideoBitDepth; - } - - if (!string.IsNullOrEmpty(codec)) - { - var value = BaseRequest.GetOption(codec, "videobitdepth"); - if (!string.IsNullOrEmpty(value) - && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) - { - return result; - } - } - - return null; - } - - public int? GetRequestedAudioBitDepth(string codec) - { - if (BaseRequest.MaxAudioBitDepth.HasValue) - { - return BaseRequest.MaxAudioBitDepth; - } - - if (!string.IsNullOrEmpty(codec)) - { - var value = BaseRequest.GetOption(codec, "audiobitdepth"); - if (!string.IsNullOrEmpty(value) - && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) - { - return result; - } - } - - return null; - } - - public int? GetRequestedAudioChannels(string codec) - { - if (BaseRequest.MaxAudioChannels.HasValue) - { - return BaseRequest.MaxAudioChannels; - } - - if (BaseRequest.AudioChannels.HasValue) - { - return BaseRequest.AudioChannels; - } - - if (!string.IsNullOrEmpty(codec)) - { - var value = BaseRequest.GetOption(codec, "audiochannels"); - if (!string.IsNullOrEmpty(value) - && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) - { - return result; - } - } - - return null; - } - public bool IsVideoRequest { get; set; } + public TranscodingJobType TranscodingType { get; set; } - public EncodingJobInfo(TranscodingJobType jobType) - { - TranscodingType = jobType; - RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - PlayableStreamFileNames = Array.Empty<string>(); - SupportedAudioCodecs = Array.Empty<string>(); - SupportedVideoCodecs = Array.Empty<string>(); - SupportedSubtitleCodecs = Array.Empty<string>(); - } + public long? StartTimeTicks => BaseRequest.StartTimeTicks; + + public bool CopyTimestamps => BaseRequest.CopyTimestamps; public bool IsSegmentedLiveStream => TranscodingType != TranscodingJobType.Progressive && !RunTimeTicks.HasValue; - public bool EnableBreakOnNonKeyFrames(string videoCodec) - { - if (TranscodingType != TranscodingJobType.Progressive) - { - if (IsSegmentedLiveStream) - { - return false; - } - - return BaseRequest.BreakOnNonKeyFrames && string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase); - } - - return false; - } - public int? TotalOutputBitrate => (OutputAudioBitrate ?? 0) + (OutputVideoBitrate ?? 0); public int? OutputWidth @@ -316,13 +167,10 @@ namespace MediaBrowser.Controller.MediaEncoding { if (VideoStream != null && VideoStream.Width.HasValue && VideoStream.Height.HasValue) { - var size = new ImageDimensions - { - Width = VideoStream.Width.Value, - Height = VideoStream.Height.Value - }; + var size = new ImageDimensions(VideoStream.Width.Value, VideoStream.Height.Value); - var newSize = DrawingUtils.Resize(size, + var newSize = DrawingUtils.Resize( + size, BaseRequest.Width ?? 0, BaseRequest.Height ?? 0, BaseRequest.MaxWidth ?? 0, @@ -346,13 +194,10 @@ namespace MediaBrowser.Controller.MediaEncoding { if (VideoStream != null && VideoStream.Width.HasValue && VideoStream.Height.HasValue) { - var size = new ImageDimensions - { - Width = VideoStream.Width.Value, - Height = VideoStream.Height.Value - }; + var size = new ImageDimensions(VideoStream.Width.Value, VideoStream.Height.Value); - var newSize = DrawingUtils.Resize(size, + var newSize = DrawingUtils.Resize( + size, BaseRequest.Width ?? 0, BaseRequest.Height ?? 0, BaseRequest.MaxWidth ?? 0, @@ -374,19 +219,19 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || EncodingHelper.IsCopyCodec(OutputAudioCodec)) { if (AudioStream != null) { return AudioStream.SampleRate; } } - else if (BaseRequest.AudioSampleRate.HasValue) { // Don't exceed what the encoder supports // Seeing issues of attempting to encode to 88200 - return Math.Min(44100, BaseRequest.AudioSampleRate.Value); + return BaseRequest.AudioSampleRate.Value; } return null; @@ -397,7 +242,8 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || EncodingHelper.IsCopyCodec(OutputAudioCodec)) { if (AudioStream != null) { @@ -405,25 +251,18 @@ namespace MediaBrowser.Controller.MediaEncoding } } - //else if (BaseRequest.AudioSampleRate.HasValue) - //{ - // // Don't exceed what the encoder supports - // // Seeing issues of attempting to encode to 88200 - // return Math.Min(44100, BaseRequest.AudioSampleRate.Value); - //} - return null; } } /// <summary> - /// Predicts the audio sample rate that will be in the output stream + /// Gets the target video level. /// </summary> public double? TargetVideoLevel { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { return VideoStream?.Level; } @@ -440,13 +279,14 @@ namespace MediaBrowser.Controller.MediaEncoding } /// <summary> - /// Predicts the audio sample rate that will be in the output stream + /// Gets the target video bit depth. /// </summary> public int? TargetVideoBitDepth { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { return VideoStream?.BitDepth; } @@ -463,7 +303,8 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { return VideoStream?.RefFrames; } @@ -473,13 +314,14 @@ namespace MediaBrowser.Controller.MediaEncoding } /// <summary> - /// Predicts the audio sample rate that will be in the output stream + /// Gets the target framerate. /// </summary> public float? TargetFramerate { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { return VideoStream == null ? null : (VideoStream.AverageFrameRate ?? VideoStream.RealFrameRate); } @@ -504,13 +346,13 @@ namespace MediaBrowser.Controller.MediaEncoding } /// <summary> - /// Predicts the audio sample rate that will be in the output stream + /// Gets the target packet length. /// </summary> public int? TargetPacketLength { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { return VideoStream?.PacketLength; } @@ -520,13 +362,13 @@ namespace MediaBrowser.Controller.MediaEncoding } /// <summary> - /// Predicts the audio sample rate that will be in the output stream + /// Gets the target video profile. /// </summary> public string TargetVideoProfile { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { return VideoStream?.Profile; } @@ -545,7 +387,8 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { return VideoStream?.CodecTag; } @@ -558,7 +401,8 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { return VideoStream?.IsAnamorphic; } @@ -571,14 +415,17 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - var codec = OutputVideoCodec; + if (VideoStream == null) + { + return null; + } - if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase)) + if (EncodingHelper.IsCopyCodec(OutputVideoCodec)) { - return VideoStream?.Codec; + return VideoStream.Codec; } - return codec; + return OutputVideoCodec; } } @@ -586,14 +433,17 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - var codec = OutputAudioCodec; + if (AudioStream == null) + { + return null; + } - if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase)) + if (EncodingHelper.IsCopyCodec(OutputAudioCodec)) { - return AudioStream?.Codec; + return AudioStream.Codec; } - return codec; + return OutputAudioCodec; } } @@ -601,7 +451,8 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { return VideoStream?.IsInterlaced; } @@ -619,7 +470,7 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { return VideoStream?.IsAVC; } @@ -636,6 +487,7 @@ namespace MediaBrowser.Controller.MediaEncoding { return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue); } + return GetMediaStreamCount(MediaStreamType.Video, 1); } } @@ -648,16 +500,26 @@ namespace MediaBrowser.Controller.MediaEncoding { return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue); } + return GetMediaStreamCount(MediaStreamType.Audio, 1); } } - public int HlsListSize + public int HlsListSize => 0; + + public bool EnableBreakOnNonKeyFrames(string videoCodec) { - get + if (TranscodingType != TranscodingJobType.Progressive) { - return 0; + if (IsSegmentedLiveStream) + { + return false; + } + + return BaseRequest.BreakOnNonKeyFrames && EncodingHelper.IsCopyCodec(videoCodec); } + + return false; } private int? GetMediaStreamCount(MediaStreamType type, int limit) @@ -672,33 +534,176 @@ namespace MediaBrowser.Controller.MediaEncoding return count; } - public IProgress<double> Progress { get; set; } - public virtual void ReportTranscodingProgress(TimeSpan? transcodingPosition, float framerate, double? percentComplete, long bytesTranscoded, int? bitRate) + public string GetMimeType(string outputPath, bool enableStreamDefault = true) { - Progress.Report(percentComplete.Value); + if (!string.IsNullOrEmpty(MimeType)) + { + return MimeType; + } + + if (enableStreamDefault) + { + return MimeTypes.GetMimeType(outputPath); + } + + return MimeTypes.GetMimeType(outputPath, null); } - public virtual void Dispose() + public bool DeInterlace(string videoCodec, bool forceDeinterlaceIfSourceIsInterlaced) { + var videoStream = VideoStream; + var isInputInterlaced = videoStream != null && videoStream.IsInterlaced; + + if (!isInputInterlaced) + { + return false; + } + + // Support general param + if (BaseRequest.DeInterlace) + { + return true; + } + + if (!string.IsNullOrEmpty(videoCodec)) + { + if (string.Equals(BaseRequest.GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return forceDeinterlaceIfSourceIsInterlaced; } - } - /// <summary> - /// Enum TranscodingJobType - /// </summary> - public enum TranscodingJobType - { - /// <summary> - /// The progressive - /// </summary> - Progressive, - /// <summary> - /// The HLS - /// </summary> - Hls, - /// <summary> - /// The dash - /// </summary> - Dash + public string[] GetRequestedProfiles(string codec) + { + if (!string.IsNullOrEmpty(BaseRequest.Profile)) + { + return BaseRequest.Profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries); + } + + if (!string.IsNullOrEmpty(codec)) + { + var profile = BaseRequest.GetOption(codec, "profile"); + + if (!string.IsNullOrEmpty(profile)) + { + return profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries); + } + } + + return Array.Empty<string>(); + } + + public string GetRequestedLevel(string codec) + { + if (!string.IsNullOrEmpty(BaseRequest.Level)) + { + return BaseRequest.Level; + } + + if (!string.IsNullOrEmpty(codec)) + { + return BaseRequest.GetOption(codec, "level"); + } + + return null; + } + + public int? GetRequestedMaxRefFrames(string codec) + { + if (BaseRequest.MaxRefFrames.HasValue) + { + return BaseRequest.MaxRefFrames; + } + + if (!string.IsNullOrEmpty(codec)) + { + var value = BaseRequest.GetOption(codec, "maxrefframes"); + if (!string.IsNullOrEmpty(value) + && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + { + return result; + } + } + + return null; + } + + public int? GetRequestedVideoBitDepth(string codec) + { + if (BaseRequest.MaxVideoBitDepth.HasValue) + { + return BaseRequest.MaxVideoBitDepth; + } + + if (!string.IsNullOrEmpty(codec)) + { + var value = BaseRequest.GetOption(codec, "videobitdepth"); + if (!string.IsNullOrEmpty(value) + && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + { + return result; + } + } + + return null; + } + + public int? GetRequestedAudioBitDepth(string codec) + { + if (BaseRequest.MaxAudioBitDepth.HasValue) + { + return BaseRequest.MaxAudioBitDepth; + } + + if (!string.IsNullOrEmpty(codec)) + { + var value = BaseRequest.GetOption(codec, "audiobitdepth"); + if (!string.IsNullOrEmpty(value) + && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + { + return result; + } + } + + return null; + } + + public int? GetRequestedAudioChannels(string codec) + { + if (!string.IsNullOrEmpty(codec)) + { + var value = BaseRequest.GetOption(codec, "audiochannels"); + if (!string.IsNullOrEmpty(value) + && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + { + return result; + } + } + + if (BaseRequest.MaxAudioChannels.HasValue) + { + return BaseRequest.MaxAudioChannels; + } + + if (BaseRequest.AudioChannels.HasValue) + { + return BaseRequest.AudioChannels; + } + + if (BaseRequest.TranscodingMaxAudioChannels.HasValue) + { + return BaseRequest.TranscodingMaxAudioChannels; + } + + return null; + } + + public virtual void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate) + { + Progress.Report(percentComplete.Value); + } } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs deleted file mode 100644 index be97c75a2..000000000 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs +++ /dev/null @@ -1,271 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Services; - -namespace MediaBrowser.Controller.MediaEncoding -{ - public class EncodingJobOptions : BaseEncodingJobOptions - { - public string OutputDirectory { get; set; } - public string ItemId { get; set; } - - public string TempDirectory { get; set; } - public bool ReadInputAtNativeFramerate { get; set; } - - /// <summary> - /// Gets a value indicating whether this instance has fixed resolution. - /// </summary> - /// <value><c>true</c> if this instance has fixed resolution; otherwise, <c>false</c>.</value> - public bool HasFixedResolution => Width.HasValue || Height.HasValue; - - public DeviceProfile DeviceProfile { get; set; } - - public EncodingJobOptions(StreamInfo info, DeviceProfile deviceProfile) - { - Container = info.Container; - StartTimeTicks = info.StartPositionTicks; - MaxWidth = info.MaxWidth; - MaxHeight = info.MaxHeight; - MaxFramerate = info.MaxFramerate; - Id = info.ItemId; - MediaSourceId = info.MediaSourceId; - AudioCodec = info.TargetAudioCodec.FirstOrDefault(); - MaxAudioChannels = info.GlobalMaxAudioChannels; - AudioBitRate = info.AudioBitrate; - AudioSampleRate = info.TargetAudioSampleRate; - DeviceProfile = deviceProfile; - VideoCodec = info.TargetVideoCodec.FirstOrDefault(); - VideoBitRate = info.VideoBitrate; - AudioStreamIndex = info.AudioStreamIndex; - SubtitleMethod = info.SubtitleDeliveryMethod; - Context = info.Context; - TranscodingMaxAudioChannels = info.TranscodingMaxAudioChannels; - - if (info.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External) - { - SubtitleStreamIndex = info.SubtitleStreamIndex; - } - StreamOptions = info.StreamOptions; - } - } - - // For now until api and media encoding layers are unified - public class BaseEncodingJobOptions - { - /// <summary> - /// Gets or sets the id. - /// </summary> - /// <value>The id.</value> - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public Guid Id { get; set; } - - [ApiMember(Name = "MediaSourceId", Description = "The media version id, if playing an alternate version", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string MediaSourceId { get; set; } - - [ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string DeviceId { get; set; } - - [ApiMember(Name = "Container", Description = "Container", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string Container { get; set; } - - /// <summary> - /// Gets or sets the audio codec. - /// </summary> - /// <value>The audio codec.</value> - [ApiMember(Name = "AudioCodec", Description = "Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string AudioCodec { get; set; } - - [ApiMember(Name = "EnableAutoStreamCopy", Description = "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool EnableAutoStreamCopy { get; set; } - - public bool AllowVideoStreamCopy { get; set; } - public bool AllowAudioStreamCopy { get; set; } - public bool BreakOnNonKeyFrames { get; set; } - - /// <summary> - /// Gets or sets the audio sample rate. - /// </summary> - /// <value>The audio sample rate.</value> - [ApiMember(Name = "AudioSampleRate", Description = "Optional. Specify a specific audio sample rate, e.g. 44100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? AudioSampleRate { get; set; } - - public int? MaxAudioBitDepth { get; set; } - - /// <summary> - /// Gets or sets the audio bit rate. - /// </summary> - /// <value>The audio bit rate.</value> - [ApiMember(Name = "AudioBitRate", Description = "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? AudioBitRate { get; set; } - - /// <summary> - /// Gets or sets the audio channels. - /// </summary> - /// <value>The audio channels.</value> - [ApiMember(Name = "AudioChannels", Description = "Optional. Specify a specific number of audio channels to encode to, e.g. 2", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? AudioChannels { get; set; } - - [ApiMember(Name = "MaxAudioChannels", Description = "Optional. Specify a maximum number of audio channels to encode to, e.g. 2", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? MaxAudioChannels { get; set; } - - [ApiMember(Name = "Static", Description = "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool Static { get; set; } - - /// <summary> - /// Gets or sets the profile. - /// </summary> - /// <value>The profile.</value> - [ApiMember(Name = "Profile", Description = "Optional. Specify a specific h264 profile, e.g. main, baseline, high.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string Profile { get; set; } - - /// <summary> - /// Gets or sets the level. - /// </summary> - /// <value>The level.</value> - [ApiMember(Name = "Level", Description = "Optional. Specify a level for the h264 profile, e.g. 3, 3.1.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string Level { get; set; } - - /// <summary> - /// Gets or sets the framerate. - /// </summary> - /// <value>The framerate.</value> - [ApiMember(Name = "Framerate", Description = "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", IsRequired = false, DataType = "double", ParameterType = "query", Verb = "GET")] - public float? Framerate { get; set; } - - [ApiMember(Name = "MaxFramerate", Description = "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", IsRequired = false, DataType = "double", ParameterType = "query", Verb = "GET")] - public float? MaxFramerate { get; set; } - - [ApiMember(Name = "CopyTimestamps", Description = "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool CopyTimestamps { get; set; } - - /// <summary> - /// Gets or sets the start time ticks. - /// </summary> - /// <value>The start time ticks.</value> - [ApiMember(Name = "StartTimeTicks", Description = "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public long? StartTimeTicks { get; set; } - - /// <summary> - /// Gets or sets the width. - /// </summary> - /// <value>The width.</value> - [ApiMember(Name = "Width", Description = "Optional. The fixed horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? Width { get; set; } - - /// <summary> - /// Gets or sets the height. - /// </summary> - /// <value>The height.</value> - [ApiMember(Name = "Height", Description = "Optional. The fixed vertical resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? Height { get; set; } - - /// <summary> - /// Gets or sets the width of the max. - /// </summary> - /// <value>The width of the max.</value> - [ApiMember(Name = "MaxWidth", Description = "Optional. The maximum horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? MaxWidth { get; set; } - - /// <summary> - /// Gets or sets the height of the max. - /// </summary> - /// <value>The height of the max.</value> - [ApiMember(Name = "MaxHeight", Description = "Optional. The maximum vertical resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? MaxHeight { get; set; } - - /// <summary> - /// Gets or sets the video bit rate. - /// </summary> - /// <value>The video bit rate.</value> - [ApiMember(Name = "VideoBitRate", Description = "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? VideoBitRate { get; set; } - - /// <summary> - /// Gets or sets the index of the subtitle stream. - /// </summary> - /// <value>The index of the subtitle stream.</value> - [ApiMember(Name = "SubtitleStreamIndex", Description = "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? SubtitleStreamIndex { get; set; } - - [ApiMember(Name = "SubtitleMethod", Description = "Optional. Specify the subtitle delivery method.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public SubtitleDeliveryMethod SubtitleMethod { get; set; } - - [ApiMember(Name = "MaxRefFrames", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? MaxRefFrames { get; set; } - - [ApiMember(Name = "MaxVideoBitDepth", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? MaxVideoBitDepth { get; set; } - public bool RequireAvc { get; set; } - public bool DeInterlace { get; set; } - public bool RequireNonAnamorphic { get; set; } - public int? TranscodingMaxAudioChannels { get; set; } - public int? CpuCoreLimit { get; set; } - - public string LiveStreamId { get; set; } - - public bool EnableMpegtsM2TsMode { get; set; } - - /// <summary> - /// Gets or sets the video codec. - /// </summary> - /// <value>The video codec.</value> - [ApiMember(Name = "VideoCodec", Description = "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h264, mpeg4, theora, vpx, wmv.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string VideoCodec { get; set; } - - public string SubtitleCodec { get; set; } - - public string TranscodeReasons { get; set; } - - /// <summary> - /// Gets or sets the index of the audio stream. - /// </summary> - /// <value>The index of the audio stream.</value> - [ApiMember(Name = "AudioStreamIndex", Description = "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? AudioStreamIndex { get; set; } - - /// <summary> - /// Gets or sets the index of the video stream. - /// </summary> - /// <value>The index of the video stream.</value> - [ApiMember(Name = "VideoStreamIndex", Description = "Optional. The index of the video stream to use. If omitted the first video stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? VideoStreamIndex { get; set; } - - public EncodingContext Context { get; set; } - - public Dictionary<string, string> StreamOptions { get; set; } - - public string GetOption(string qualifier, string name) - { - var value = GetOption(qualifier + "-" + name); - - if (string.IsNullOrEmpty(value)) - { - value = GetOption(name); - } - - return value; - } - - public string GetOption(string name) - { - if (StreamOptions.TryGetValue(name, out var value)) - { - return value; - } - - return null; - } - - public BaseEncodingJobOptions() - { - EnableAutoStreamCopy = true; - AllowVideoStreamCopy = true; - AllowAudioStreamCopy = true; - Context = EncodingContext.Streaming; - StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } - } -} diff --git a/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs new file mode 100644 index 000000000..7ce707b19 --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs @@ -0,0 +1,23 @@ +namespace MediaBrowser.Controller.MediaEncoding +{ + /// <summary> + /// Enum FilterOptionType. + /// </summary> + public enum FilterOptionType + { + /// <summary> + /// The scale_cuda_format. + /// </summary> + ScaleCudaFormat = 0, + + /// <summary> + /// The tonemap_cuda_name. + /// </summary> + TonemapCudaName = 1, + + /// <summary> + /// The tonemap_opencl_bt2390. + /// </summary> + TonemapOpenclBt2390 = 2 + } +} diff --git a/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs new file mode 100644 index 000000000..c38e7ec3b --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs @@ -0,0 +1,21 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +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); + } +} diff --git a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs index e560999e8..8ce40a58d 100644 --- a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs +++ b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -12,6 +16,13 @@ namespace MediaBrowser.Controller.MediaEncoding /// <summary> /// Refreshes the chapter images. /// </summary> - Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, List<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken); + /// <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 057e43910..e6511ca8d 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -1,21 +1,23 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Controller.MediaEncoding { /// <summary> - /// Interface IMediaEncoder + /// Interface IMediaEncoder. /// </summary> public interface IMediaEncoder : ITranscoderSupport { - string EncoderLocationType { get; } - /// <summary> /// Gets the encoder path. /// </summary> @@ -23,13 +25,47 @@ namespace MediaBrowser.Controller.MediaEncoding string EncoderPath { get; } /// <summary> - /// Supportses the decoder. + /// Whether given encoder codec is supported. + /// </summary> + /// <param name="encoder">The encoder.</param> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> + bool SupportsEncoder(string encoder); + + /// <summary> + /// Whether given decoder codec is supported. /// </summary> /// <param name="decoder">The decoder.</param> /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> bool SupportsDecoder(string decoder); /// <summary> + /// Whether given hardware acceleration type is supported. + /// </summary> + /// <param name="hwaccel">The hwaccel.</param> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> + bool SupportsHwaccel(string hwaccel); + + /// <summary> + /// Whether given filter is supported. + /// </summary> + /// <param name="filter">The filter.</param> + /// <returns><c>true</c> if the filter is supported, <c>false</c> otherwise.</returns> + bool SupportsFilter(string filter); + + /// <summary> + /// Whether filter is supported with the given option. + /// </summary> + /// <param name="option">The option.</param> + /// <returns><c>true</c> if the filter is supported, <c>false</c> otherwise.</returns> + bool SupportsFilterWithOption(FilterOptionType option); + + /// <summary> + /// Get the version of media encoder. + /// </summary> + /// <returns>The version of media encoder.</returns> + Version GetMediaEncoderVersion(); + + /// <summary> /// Extracts the audio image. /// </summary> /// <param name="path">The path.</param> @@ -41,17 +77,48 @@ namespace MediaBrowser.Controller.MediaEncoding /// <summary> /// Extracts the video image. /// </summary> - Task<string> ExtractVideoImage(string[] inputFiles, string container, MediaProtocol protocol, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken); + /// <param name="inputFile">Input file.</param> + /// <param name="container">Video container type.</param> + /// <param name="mediaSource">Media source information.</param> + /// <param name="videoStream">Media stream information.</param> + /// <param name="threedFormat">Video 3D format.</param> + /// <param name="offset">Time offset.</param> + /// <param name="cancellationToken">CancellationToken to use for operation.</param> + /// <returns>Location of video image.</returns> + Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken); - Task<string> ExtractVideoImage(string[] inputFiles, string container, MediaProtocol protocol, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken); + /// <summary> + /// Extracts the video image. + /// </summary> + /// <param name="inputFile">Input file.</param> + /// <param name="container">Video container type.</param> + /// <param name="mediaSource">Media source information.</param> + /// <param name="imageStream">Media stream information.</param> + /// <param name="imageStreamIndex">Index of the stream to extract from.</param> + /// <param name="outputExtension">The extension of the file to write, including the '.'.</param> + /// <param name="cancellationToken">CancellationToken to use for operation.</param> + /// <returns>Location of video image.</returns> + Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, string outputExtension, CancellationToken cancellationToken); /// <summary> /// Extracts the video images on interval. /// </summary> - Task ExtractVideoImagesOnInterval(string[] inputFiles, + /// <param name="inputFile">Input file.</param> + /// <param name="container">Video container type.</param> + /// <param name="videoStream">Media stream information.</param> + /// <param name="mediaSource">Media source information.</param> + /// <param name="threedFormat">Video 3D format.</param> + /// <param name="interval">Time interval.</param> + /// <param name="targetDirectory">Directory to write images.</param> + /// <param name="filenamePrefix">Filename prefix to use.</param> + /// <param name="maxWidth">Maximum width of image.</param> + /// <param name="cancellationToken">CancellationToken to use for operation.</param> + /// <returns>A task.</returns> + Task ExtractVideoImagesOnInterval( + string inputFile, string container, MediaStream videoStream, - MediaProtocol protocol, + MediaSourceInfo mediaSource, Video3DFormat? threedFormat, TimeSpan interval, string targetDirectory, @@ -70,10 +137,10 @@ namespace MediaBrowser.Controller.MediaEncoding /// <summary> /// Gets the input argument. /// </summary> - /// <param name="inputFiles">The input files.</param> - /// <param name="protocol">The protocol.</param> + /// <param name="inputFile">The input file.</param> + /// <param name="mediaSource">The mediaSource.</param> /// <returns>System.String.</returns> - string GetInputArgument(string[] inputFiles, MediaProtocol protocol); + string GetInputArgument(string inputFile, MediaSourceInfo mediaSource); /// <summary> /// Gets the time parameter. @@ -91,12 +158,24 @@ namespace MediaBrowser.Controller.MediaEncoding /// <returns>System.String.</returns> string EscapeSubtitleFilterPath(string path); - void Init(); + /// <summary> + /// Sets the path to find FFmpeg. + /// </summary> + void SetFFmpegPath(); + /// <summary> + /// Updates the encoder path. + /// </summary> + /// <param name="path">The path.</param> + /// <param name="pathType">The type of path.</param> void UpdateEncoderPath(string path, string pathType); - bool SupportsEncoder(string encoder); - string[] GetPlayableStreamFileNames(string path, VideoType videoType); - IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, IIsoMount isoMount, uint? titleNumber); + /// <summary> + /// Gets the primary playlist of .vob files. + /// </summary> + /// <param name="path">The to the .vob files.</param> + /// <param name="titleNumber">The title number to start with.</param> + /// <returns>A playlist.</returns> + IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber); } } diff --git a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs index 174e74f34..4483cf708 100644 --- a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System.IO; using System.Threading; using System.Threading.Tasks; @@ -11,8 +15,17 @@ namespace MediaBrowser.Controller.MediaEncoding /// <summary> /// Gets the subtitles. /// </summary> + /// <param name="item">Item to use.</param> + /// <param name="mediaSourceId">Media source.</param> + /// <param name="subtitleStreamIndex">Subtitle stream to use.</param> + /// <param name="outputFormat">Output format to use.</param> + /// <param name="startTimeTicks">Start time.</param> + /// <param name="endTimeTicks">End time.</param> + /// <param name="preserveOriginalTimestamps">Option to preserve original timestamps.</param> + /// <param name="cancellationToken">The cancellation token for the operation.</param> /// <returns>Task{Stream}.</returns> - Task<Stream> GetSubtitles(BaseItem item, + Task<Stream> GetSubtitles( + BaseItem item, string mediaSourceId, int subtitleStreamIndex, string outputFormat, @@ -25,6 +38,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// Gets the subtitle language encoding parameter. /// </summary> /// <param name="path">The path.</param> + /// <param name="language">The language.</param> /// <param name="protocol">The protocol.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>System.String.</returns> diff --git a/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs b/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs index 361dd79dc..044ba6d33 100644 --- a/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs +++ b/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.MediaEncoding { public class ImageEncodingOptions diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index b812a8ddc..933f440ac 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -1,16 +1,18 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Globalization; using System.IO; -using System.Linq; using System.Text; -using MediaBrowser.Model.Extensions; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.MediaEncoding { public class JobLogger { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly ILogger _logger; public JobLogger(ILogger logger) @@ -18,13 +20,14 @@ namespace MediaBrowser.Controller.MediaEncoding _logger = logger; } - public async void StartStreamingLog(EncodingJobInfo state, Stream source, Stream target) + public async Task StartStreamingLog(EncodingJobInfo state, Stream source, Stream target) { try { + using (target) using (var reader = new StreamReader(source)) { - while (!reader.EndOfStream) + while (!reader.EndOfStream && reader.BaseStream.CanRead) { var line = await reader.ReadLineAsync().ConfigureAwait(false); @@ -32,16 +35,24 @@ namespace MediaBrowser.Controller.MediaEncoding var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line); + // If ffmpeg process is closed, the state is disposed, so don't write to target in that case + if (!target.CanWrite) + { + break; + } + await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); + + // Check again, the stream could have been closed + if (!target.CanWrite) + { + break; + } + await target.FlushAsync().ConfigureAwait(false); } } } - catch (ObjectDisposedException) - { - //TODO Investigate and properly fix. - // Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux - } catch (Exception ex) { _logger.LogError(ex, "Error reading ffmpeg log"); @@ -75,7 +86,16 @@ namespace MediaBrowser.Controller.MediaEncoding { var rate = parts[i + 1]; - if (float.TryParse(rate, NumberStyles.Any, _usCulture, out var val)) + if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val)) + { + framerate = val; + } + } + else if (part.StartsWith("fps=", StringComparison.OrdinalIgnoreCase)) + { + var rate = part.Split('=', 2)[^1]; + + if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val)) { framerate = val; } @@ -83,24 +103,23 @@ namespace MediaBrowser.Controller.MediaEncoding else if (state.RunTimeTicks.HasValue && part.StartsWith("time=", StringComparison.OrdinalIgnoreCase)) { - var time = part.Split(new[] { '=' }, 2).Last(); + var time = part.Split('=', 2)[^1]; - if (TimeSpan.TryParse(time, _usCulture, out var val)) + if (TimeSpan.TryParse(time, CultureInfo.InvariantCulture, out var val)) { var currentMs = startMs + val.TotalMilliseconds; - var percentVal = currentMs / totalMs; - percent = 100 * percentVal; + percent = 100.0 * currentMs / totalMs; transcodingPosition = val; } } else if (part.StartsWith("size=", StringComparison.OrdinalIgnoreCase)) { - var size = part.Split(new[] { '=' }, 2).Last(); + var size = part.Split('=', 2)[^1]; int? scale = null; - if (size.IndexOf("kb", StringComparison.OrdinalIgnoreCase) != -1) + if (size.Contains("kb", StringComparison.OrdinalIgnoreCase)) { scale = 1024; size = size.Replace("kb", string.Empty, StringComparison.OrdinalIgnoreCase); @@ -108,7 +127,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (scale.HasValue) { - if (long.TryParse(size, NumberStyles.Any, _usCulture, out var val)) + if (long.TryParse(size, NumberStyles.Any, CultureInfo.InvariantCulture, out var val)) { bytesTranscoded = val * scale.Value; } @@ -116,10 +135,10 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (part.StartsWith("bitrate=", StringComparison.OrdinalIgnoreCase)) { - var rate = part.Split(new[] { '=' }, 2).Last(); + var rate = part.Split('=', 2)[^1]; int? scale = null; - if (rate.IndexOf("kbits/s", StringComparison.OrdinalIgnoreCase) != -1) + if (rate.Contains("kbits/s", StringComparison.OrdinalIgnoreCase)) { scale = 1024; rate = rate.Replace("kbits/s", string.Empty, StringComparison.OrdinalIgnoreCase); @@ -127,7 +146,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (scale.HasValue) { - if (float.TryParse(rate, NumberStyles.Any, _usCulture, out var val)) + if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val)) { bitRate = (int)Math.Ceiling(val * scale.Value); } @@ -137,7 +156,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (framerate.HasValue || percent.HasValue) { - state.ReportTranscodingProgress(transcodingPosition, 0, percent, 0, bitRate); + state.ReportTranscodingProgress(transcodingPosition, framerate, percent, bytesTranscoded, bitRate); } } } diff --git a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs index 7f842c1d0..841e7b287 100644 --- a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs +++ b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs @@ -1,53 +1,11 @@ -using System; -using System.IO; -using System.Linq; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.MediaInfo; +#pragma warning disable CS1591 namespace MediaBrowser.Controller.MediaEncoding { /// <summary> - /// Class MediaEncoderHelpers + /// Class MediaEncoderHelpers. /// </summary> public static class MediaEncoderHelpers { - /// <summary> - /// Gets the input argument. - /// </summary> - /// <param name="fileSystem">The file system.</param> - /// <param name="videoPath">The video path.</param> - /// <param name="protocol">The protocol.</param> - /// <param name="isoMount">The iso mount.</param> - /// <param name="playableStreamFileNames">The playable stream file names.</param> - /// <returns>System.String[][].</returns> - public static string[] GetInputArgument(IFileSystem fileSystem, string videoPath, MediaProtocol protocol, IIsoMount isoMount, string[] playableStreamFileNames) - { - if (playableStreamFileNames.Length > 0) - { - if (isoMount == null) - { - return GetPlayableStreamFiles(fileSystem, videoPath, playableStreamFileNames); - } - return GetPlayableStreamFiles(fileSystem, isoMount.MountedPath, playableStreamFileNames); - } - - return new[] { videoPath }; - } - - private static string[] GetPlayableStreamFiles(IFileSystem fileSystem, string rootPath, string[] filenames) - { - if (filenames.Length == 0) - { - return new string[] { }; - } - - var allFiles = fileSystem - .GetFilePaths(rootPath, true) - .ToArray(); - - return filenames.Select(name => allFiles.FirstOrDefault(f => string.Equals(Path.GetFileName(f), name, StringComparison.OrdinalIgnoreCase))) - .Where(f => !string.IsNullOrEmpty(f)) - .ToArray(); - } } } diff --git a/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs b/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs index b78ef0b80..1dd8bcf31 100644 --- a/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs +++ b/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs @@ -1,21 +1,18 @@ -using System; +#nullable disable + +#pragma warning disable CS1591 + using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.IO; namespace MediaBrowser.Controller.MediaEncoding { public class MediaInfoRequest { public MediaSourceInfo MediaSource { get; set; } + public bool ExtractChapters { get; set; } - public DlnaProfileType MediaType { get; set; } - public IIsoMount MountedIso { get; set; } - public string[] PlayableStreamFileNames { get; set; } - public MediaInfoRequest() - { - PlayableStreamFileNames = Array.Empty<string>(); - } + public DlnaProfileType MediaType { get; set; } } } diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingJobType.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingJobType.cs new file mode 100644 index 000000000..66b628371 --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/TranscodingJobType.cs @@ -0,0 +1,23 @@ +namespace MediaBrowser.Controller.MediaEncoding +{ + /// <summary> + /// Enum TranscodingJobType. + /// </summary> + public enum TranscodingJobType + { + /// <summary> + /// The progressive. + /// </summary> + Progressive, + + /// <summary> + /// The HLS. + /// </summary> + Hls, + + /// <summary> + /// The dash. + /// </summary> + Dash + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs b/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs deleted file mode 100644 index 64c2294e3..000000000 --- a/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using MediaBrowser.Model.Services; - -namespace MediaBrowser.Controller.Net -{ - public class AuthenticatedAttribute : Attribute, IHasRequestFilter, IAuthenticationAttributes - { - public static IAuthService AuthService { get; set; } - - /// <summary> - /// Gets or sets the roles. - /// </summary> - /// <value>The roles.</value> - public string Roles { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether [escape parental control]. - /// </summary> - /// <value><c>true</c> if [escape parental control]; otherwise, <c>false</c>.</value> - public bool EscapeParentalControl { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether [allow before startup wizard]. - /// </summary> - /// <value><c>true</c> if [allow before startup wizard]; otherwise, <c>false</c>.</value> - public bool AllowBeforeStartupWizard { get; set; } - - public bool AllowLocal { get; set; } - - /// <summary> - /// The request filter is executed before the service. - /// </summary> - /// <param name="request">The http request wrapper</param> - /// <param name="response">The http response wrapper</param> - /// <param name="requestDto">The request DTO</param> - public void RequestFilter(IRequest request, IResponse response, object requestDto) - { - AuthService.Authenticate(request, this); - } - - /// <summary> - /// Order in which Request Filters are executed. - /// <0 Executed before global request filters - /// >0 Executed after global request filters - /// </summary> - /// <value>The priority.</value> - public int Priority => 0; - - public string[] GetRoles() - { - return (Roles ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - } - - public bool AllowLocalOnly { get; set; } - } - - public interface IAuthenticationAttributes - { - bool EscapeParentalControl { get; } - bool AllowBeforeStartupWizard { get; } - bool AllowLocal { get; } - bool AllowLocalOnly { get; } - - string[] GetRoles(); - } -} diff --git a/MediaBrowser.Controller/Net/AuthorizationInfo.cs b/MediaBrowser.Controller/Net/AuthorizationInfo.cs index 3e004763d..2452b25ab 100644 --- a/MediaBrowser.Controller/Net/AuthorizationInfo.cs +++ b/MediaBrowser.Controller/Net/AuthorizationInfo.cs @@ -1,42 +1,69 @@ +#nullable disable + using System; -using MediaBrowser.Controller.Entities; +using Jellyfin.Data.Entities; namespace MediaBrowser.Controller.Net { + /// <summary> + /// The request authorization info. + /// </summary> public class AuthorizationInfo { /// <summary> - /// Gets or sets the user identifier. + /// Gets the user identifier. /// </summary> /// <value>The user identifier.</value> - public Guid UserId => User == null ? Guid.Empty : User.Id; + public Guid UserId => User?.Id ?? Guid.Empty; /// <summary> /// Gets or sets the device identifier. /// </summary> /// <value>The device identifier.</value> public string DeviceId { get; set; } + /// <summary> /// Gets or sets the device. /// </summary> /// <value>The device.</value> public string Device { get; set; } + /// <summary> /// Gets or sets the client. /// </summary> /// <value>The client.</value> public string Client { get; set; } + /// <summary> /// Gets or sets the version. /// </summary> /// <value>The version.</value> public string Version { get; set; } + /// <summary> /// Gets or sets the token. /// </summary> /// <value>The token.</value> public string Token { get; set; } + /// <summary> + /// Gets or sets a value indicating whether the authorization is from an api key. + /// </summary> + public bool IsApiKey { get; set; } + + /// <summary> + /// Gets or sets the user making the request. + /// </summary> public User User { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the token is authenticated. + /// </summary> + public bool IsAuthenticated { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the request has a token. + /// </summary> + public bool HasToken { get; set; } } } diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index 4242a00e2..0813a8e7d 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591, SA1306, SA1401 + using System; using System.Collections.Generic; using System.Globalization; @@ -6,12 +10,13 @@ using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Net; +using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Net { /// <summary> - /// Starts sending data over a web socket periodically when a message is received, and then stops when a corresponding stop message is received + /// Starts sending data over a web socket periodically when a message is received, and then stops when a corresponding stop message is received. /// </summary> /// <typeparam name="TReturnDataType">The type of the T return data type.</typeparam> /// <typeparam name="TStateType">The type of the T state type.</typeparam> @@ -20,30 +25,17 @@ namespace MediaBrowser.Controller.Net where TReturnDataType : class { /// <summary> - /// The _active connections - /// </summary> - protected readonly List<Tuple<IWebSocketConnection, CancellationTokenSource, Timer, TStateType>> ActiveConnections = - new List<Tuple<IWebSocketConnection, CancellationTokenSource, Timer, TStateType>>(); - - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - protected abstract string Name { get; } - - /// <summary> - /// Gets the data to send. + /// The _active connections. /// </summary> - /// <param name="state">The state.</param> - /// <returns>Task{`1}.</returns> - protected abstract Task<TReturnDataType> GetDataToSend(TStateType state, CancellationToken cancellationToken); + private readonly List<Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>> _activeConnections = + new List<Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>>(); /// <summary> - /// The logger + /// The logger. /// </summary> - protected ILogger Logger; + protected ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> Logger; - protected BasePeriodicWebSocketListener(ILogger logger) + protected BasePeriodicWebSocketListener(ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> logger) { if (logger == null) { @@ -54,62 +46,71 @@ namespace MediaBrowser.Controller.Net } /// <summary> + /// Gets the type used for the messages sent to the client. + /// </summary> + /// <value>The type.</value> + protected abstract SessionMessageType Type { get; } + + /// <summary> + /// Gets the message type received from the client to start sending messages. + /// </summary> + /// <value>The type.</value> + protected abstract SessionMessageType StartType { get; } + + /// <summary> + /// Gets the message type received from the client to stop sending messages. + /// </summary> + /// <value>The type.</value> + protected abstract SessionMessageType StopType { get; } + + /// <summary> + /// Gets the data to send. + /// </summary> + /// <returns>Task{`1}.</returns> + protected abstract Task<TReturnDataType> GetDataToSend(); + + /// <summary> /// Processes the message. /// </summary> /// <param name="message">The message.</param> /// <returns>Task.</returns> - public Task ProcessMessage(WebSocketMessageInfo message) + public Task ProcessMessageAsync(WebSocketMessageInfo message) { if (message == null) { throw new ArgumentNullException(nameof(message)); } - if (string.Equals(message.MessageType, Name + "Start", StringComparison.OrdinalIgnoreCase)) + if (message.MessageType == StartType) { Start(message); } - if (string.Equals(message.MessageType, Name + "Stop", StringComparison.OrdinalIgnoreCase)) + if (message.MessageType == StopType) { Stop(message); } - return Task.FromResult(true); + return Task.CompletedTask; } - protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); - - protected virtual bool SendOnTimer => false; - - protected virtual void ParseMessageParams(string[] values) - { - - } + /// <inheritdoc /> + public Task ProcessWebSocketConnectedAsync(IWebSocketConnection connection) => Task.CompletedTask; /// <summary> - /// Starts sending messages over a web socket + /// Starts sending messages over a web socket. /// </summary> /// <param name="message">The message.</param> private void Start(WebSocketMessageInfo message) { var vals = message.Data.Split(','); - var dueTimeMs = long.Parse(vals[0], UsCulture); - var periodMs = long.Parse(vals[1], UsCulture); - - if (vals.Length > 2) - { - ParseMessageParams(vals.Skip(2).ToArray()); - } + var dueTimeMs = long.Parse(vals[0], CultureInfo.InvariantCulture); + var periodMs = long.Parse(vals[1], CultureInfo.InvariantCulture); var cancellationTokenSource = new CancellationTokenSource(); - Logger.LogDebug("{1} Begin transmitting over websocket to {0}", message.Connection.RemoteEndPoint, GetType().Name); - - var timer = SendOnTimer ? - new Timer(TimerCallback, message.Connection, Timeout.Infinite, Timeout.Infinite) : - null; + Logger.LogDebug("WS {1} begin transmitting to {0}", message.Connection.RemoteEndPoint, GetType().Name); var state = new TStateType { @@ -117,58 +118,24 @@ namespace MediaBrowser.Controller.Net InitialDelayMs = dueTimeMs }; - lock (ActiveConnections) + lock (_activeConnections) { - ActiveConnections.Add(new Tuple<IWebSocketConnection, CancellationTokenSource, Timer, TStateType>(message.Connection, cancellationTokenSource, timer, state)); + _activeConnections.Add(new Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>(message.Connection, cancellationTokenSource, state)); } - - if (timer != null) - { - timer.Change(TimeSpan.FromMilliseconds(dueTimeMs), TimeSpan.FromMilliseconds(periodMs)); - } - } - - /// <summary> - /// Timers the callback. - /// </summary> - /// <param name="state">The state.</param> - private void TimerCallback(object state) - { - var connection = (IWebSocketConnection)state; - - Tuple<IWebSocketConnection, CancellationTokenSource, Timer, TStateType> tuple; - - lock (ActiveConnections) - { - tuple = ActiveConnections.FirstOrDefault(c => c.Item1 == connection); - } - - if (tuple == null) - { - return; - } - - if (connection.State != WebSocketState.Open || tuple.Item2.IsCancellationRequested) - { - DisposeConnection(tuple); - return; - } - - SendData(tuple); } - protected void SendData(bool force) + protected async Task SendData(bool force) { - Tuple<IWebSocketConnection, CancellationTokenSource, Timer, TStateType>[] tuples; + Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>[] tuples; - lock (ActiveConnections) + lock (_activeConnections) { - tuples = ActiveConnections + tuples = _activeConnections .Where(c => { if (c.Item1.State == WebSocketState.Open && !c.Item2.IsCancellationRequested) { - var state = c.Item4; + var state = c.Item3; if (force || (DateTime.UtcNow - state.DateLastSendUtc).TotalMilliseconds >= state.IntervalMs) { @@ -181,32 +148,39 @@ namespace MediaBrowser.Controller.Net .ToArray(); } - foreach (var tuple in tuples) + IEnumerable<Task> GetTasks() { - SendData(tuple); + foreach (var tuple in tuples) + { + yield return SendData(tuple); + } } + + await Task.WhenAll(GetTasks()).ConfigureAwait(false); } - private async void SendData(Tuple<IWebSocketConnection, CancellationTokenSource, Timer, TStateType> tuple) + private async Task SendData(Tuple<IWebSocketConnection, CancellationTokenSource, TStateType> tuple) { var connection = tuple.Item1; try { - var state = tuple.Item4; + var state = tuple.Item3; var cancellationToken = tuple.Item2.Token; - var data = await GetDataToSend(state, cancellationToken).ConfigureAwait(false); + var data = await GetDataToSend().ConfigureAwait(false); if (data != null) { - await connection.SendAsync(new WebSocketMessage<TReturnDataType> - { - MessageType = Name, - Data = data - - }, cancellationToken).ConfigureAwait(false); + await connection.SendAsync( + new WebSocketMessage<TReturnDataType> + { + MessageId = Guid.NewGuid(), + MessageType = Type, + Data = data + }, + cancellationToken).ConfigureAwait(false); state.DateLastSendUtc = DateTime.UtcNow; } @@ -220,20 +194,20 @@ namespace MediaBrowser.Controller.Net } catch (Exception ex) { - Logger.LogError(ex, "Error sending web socket message {Name}", Name); + Logger.LogError(ex, "Error sending web socket message {Name}", Type); DisposeConnection(tuple); } } /// <summary> - /// Stops sending messages over a web socket + /// Stops sending messages over a web socket. /// </summary> /// <param name="message">The message.</param> private void Stop(WebSocketMessageInfo message) { - lock (ActiveConnections) + lock (_activeConnections) { - var connection = ActiveConnections.FirstOrDefault(c => c.Item1 == message.Connection); + var connection = _activeConnections.FirstOrDefault(c => c.Item1 == message.Connection); if (connection != null) { @@ -246,23 +220,12 @@ namespace MediaBrowser.Controller.Net /// Disposes the connection. /// </summary> /// <param name="connection">The connection.</param> - private void DisposeConnection(Tuple<IWebSocketConnection, CancellationTokenSource, Timer, TStateType> connection) + private void DisposeConnection(Tuple<IWebSocketConnection, CancellationTokenSource, TStateType> connection) { - Logger.LogDebug("{1} stop transmitting over websocket to {0}", connection.Item1.RemoteEndPoint, GetType().Name); + Logger.LogDebug("WS {1} stop transmitting to {0}", connection.Item1.RemoteEndPoint, GetType().Name); - var timer = connection.Item3; - - if (timer != null) - { - try - { - timer.Dispose(); - } - catch (ObjectDisposedException) - { - //TODO Investigate and properly fix. - } - } + // TODO disposing the connection seems to break websockets in subtle ways, so what is the purpose of this function really... + // connection.Item1.Dispose(); try { @@ -271,12 +234,12 @@ namespace MediaBrowser.Controller.Net } catch (ObjectDisposedException) { - //TODO Investigate and properly fix. + // TODO Investigate and properly fix. } - lock (ActiveConnections) + lock (_activeConnections) { - ActiveConnections.Remove(connection); + _activeConnections.Remove(connection); } } @@ -288,9 +251,9 @@ namespace MediaBrowser.Controller.Net { if (dispose) { - lock (ActiveConnections) + lock (_activeConnections) { - foreach (var connection in ActiveConnections.ToArray()) + foreach (var connection in _activeConnections.ToArray()) { DisposeConnection(connection); } @@ -304,13 +267,7 @@ namespace MediaBrowser.Controller.Net public void Dispose() { Dispose(true); + GC.SuppressFinalize(this); } } - - public class WebSocketListenerState - { - public DateTime DateLastSendUtc { get; set; } - public long InitialDelayMs { get; set; } - public long IntervalMs { get; set; } - } } diff --git a/MediaBrowser.Controller/Net/IAuthService.cs b/MediaBrowser.Controller/Net/IAuthService.cs index 142f1d91c..a7da740e0 100644 --- a/MediaBrowser.Controller/Net/IAuthService.cs +++ b/MediaBrowser.Controller/Net/IAuthService.cs @@ -1,9 +1,18 @@ -using MediaBrowser.Model.Services; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net { + /// <summary> + /// IAuthService. + /// </summary> public interface IAuthService { - void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues); + /// <summary> + /// Authenticate request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>Authorization information. Null if unauthenticated.</returns> + Task<AuthorizationInfo> Authenticate(HttpRequest request); } } diff --git a/MediaBrowser.Controller/Net/IAuthorizationContext.cs b/MediaBrowser.Controller/Net/IAuthorizationContext.cs index 61598391f..5c6ca43d1 100644 --- a/MediaBrowser.Controller/Net/IAuthorizationContext.cs +++ b/MediaBrowser.Controller/Net/IAuthorizationContext.cs @@ -1,21 +1,25 @@ -using MediaBrowser.Model.Services; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net { + /// <summary> + /// IAuthorization context. + /// </summary> public interface IAuthorizationContext { /// <summary> /// Gets the authorization information. /// </summary> /// <param name="requestContext">The request context.</param> - /// <returns>AuthorizationInfo.</returns> - AuthorizationInfo GetAuthorizationInfo(object requestContext); + /// <returns>A task containing the authorization info.</returns> + Task<AuthorizationInfo> GetAuthorizationInfo(HttpContext requestContext); /// <summary> /// Gets the authorization information. /// </summary> /// <param name="requestContext">The request context.</param> - /// <returns>AuthorizationInfo.</returns> - AuthorizationInfo GetAuthorizationInfo(IRequest requestContext); + /// <returns>A <see cref="Task"/> containing the authorization info.</returns> + Task<AuthorizationInfo> GetAuthorizationInfo(HttpRequest requestContext); } } diff --git a/MediaBrowser.Controller/Net/IHasResultFactory.cs b/MediaBrowser.Controller/Net/IHasResultFactory.cs deleted file mode 100644 index b8cf8cd78..000000000 --- a/MediaBrowser.Controller/Net/IHasResultFactory.cs +++ /dev/null @@ -1,17 +0,0 @@ -using MediaBrowser.Model.Services; - -namespace MediaBrowser.Controller.Net -{ - /// <summary> - /// Interface IHasResultFactory - /// Services that require a ResultFactory should implement this - /// </summary> - public interface IHasResultFactory : IRequiresRequest - { - /// <summary> - /// Gets or sets the result factory. - /// </summary> - /// <value>The result factory.</value> - IHttpResultFactory ResultFactory { get; set; } - } -} diff --git a/MediaBrowser.Controller/Net/IHttpResultFactory.cs b/MediaBrowser.Controller/Net/IHttpResultFactory.cs deleted file mode 100644 index ff9ecf8af..000000000 --- a/MediaBrowser.Controller/Net/IHttpResultFactory.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Services; - -namespace MediaBrowser.Controller.Net -{ - /// <summary> - /// Interface IHttpResultFactory - /// </summary> - public interface IHttpResultFactory - { - /// <summary> - /// Gets the result. - /// </summary> - /// <param name="content">The content.</param> - /// <param name="contentType">Type of the content.</param> - /// <param name="responseHeaders">The response headers.</param> - /// <returns>System.Object.</returns> - object GetResult(string content, string contentType, IDictionary<string, string> responseHeaders = null); - - object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary<string, string> responseHeaders = null); - object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary<string, string> responseHeaders = null); - object GetResult(IRequest requestContext, string content, string contentType, IDictionary<string, string> responseHeaders = null); - - object GetRedirectResult(string url); - - object GetResult<T>(IRequest requestContext, T result, IDictionary<string, string> responseHeaders = null) - where T : class; - - /// <summary> - /// Gets the static result. - /// </summary> - /// <param name="requestContext">The request context.</param> - /// <param name="cacheKey">The cache key.</param> - /// <param name="lastDateModified">The last date modified.</param> - /// <param name="cacheDuration">Duration of the cache.</param> - /// <param name="contentType">Type of the content.</param> - /// <param name="factoryFn">The factory fn.</param> - /// <param name="responseHeaders">The response headers.</param> - /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param> - /// <returns>System.Object.</returns> - Task<object> GetStaticResult(IRequest requestContext, - Guid cacheKey, - DateTime? lastDateModified, - TimeSpan? cacheDuration, - string contentType, Func<Task<Stream>> factoryFn, - IDictionary<string, string> responseHeaders = null, - bool isHeadRequest = false); - - /// <summary> - /// Gets the static result. - /// </summary> - /// <param name="requestContext">The request context.</param> - /// <param name="options">The options.</param> - /// <returns>System.Object.</returns> - Task<object> GetStaticResult(IRequest requestContext, StaticResultOptions options); - - /// <summary> - /// Gets the static file result. - /// </summary> - /// <param name="requestContext">The request context.</param> - /// <param name="path">The path.</param> - /// <param name="fileShare">The file share.</param> - /// <returns>System.Object.</returns> - Task<object> GetStaticFileResult(IRequest requestContext, string path, FileShareMode fileShare = FileShareMode.Read); - - /// <summary> - /// Gets the static file result. - /// </summary> - /// <param name="requestContext">The request context.</param> - /// <param name="options">The options.</param> - /// <returns>System.Object.</returns> - Task<object> GetStaticFileResult(IRequest requestContext, - StaticFileResultOptions options); - } -} diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs deleted file mode 100644 index f41303007..000000000 --- a/MediaBrowser.Controller/Net/IHttpServer.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Services; - -namespace MediaBrowser.Controller.Net -{ - /// <summary> - /// Interface IHttpServer - /// </summary> - public interface IHttpServer : IDisposable - { - /// <summary> - /// Gets the URL prefix. - /// </summary> - /// <value>The URL prefix.</value> - string[] UrlPrefixes { get; } - - /// <summary> - /// Stops this instance. - /// </summary> - void Stop(); - - /// <summary> - /// Occurs when [web socket connected]. - /// </summary> - event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected; - - /// <summary> - /// Inits this instance. - /// </summary> - void Init(IEnumerable<IService> services, IEnumerable<IWebSocketListener> listener); - - /// <summary> - /// If set, all requests will respond with this message - /// </summary> - string GlobalResponse { get; set; } - } -} diff --git a/MediaBrowser.Controller/Net/ISessionContext.cs b/MediaBrowser.Controller/Net/ISessionContext.cs index 5c3c19f6b..b48181b3f 100644 --- a/MediaBrowser.Controller/Net/ISessionContext.cs +++ b/MediaBrowser.Controller/Net/ISessionContext.cs @@ -1,15 +1,20 @@ -using MediaBrowser.Controller.Entities; +#pragma warning disable CS1591 + +using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net { public interface ISessionContext { - SessionInfo GetSession(object requestContext); - User GetUser(object requestContext); + Task<SessionInfo> GetSession(object requestContext); + + Task<User?> GetUser(object requestContext); + + Task<SessionInfo> GetSession(HttpContext requestContext); - SessionInfo GetSession(IRequest requestContext); - User GetUser(IRequest requestContext); + Task<User?> GetUser(HttpContext requestContext); } } diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs index a09b2f7a2..c8c5caf80 100644 --- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs +++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs @@ -1,24 +1,21 @@ +#pragma warning disable CS1591 + using System; +using System.Net; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Net; -using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net { - public interface IWebSocketConnection : IDisposable + public interface IWebSocketConnection { /// <summary> /// Occurs when [closed]. /// </summary> - event EventHandler<EventArgs> Closed; - - /// <summary> - /// Gets the id. - /// </summary> - /// <value>The id.</value> - Guid Id { get; } + event EventHandler<EventArgs>? Closed; /// <summary> /// Gets the last activity date. @@ -27,21 +24,22 @@ namespace MediaBrowser.Controller.Net DateTime LastActivityDate { get; } /// <summary> - /// Gets or sets the URL. + /// Gets or sets the date of last Keeplive received. /// </summary> - /// <value>The URL.</value> - string Url { get; set; } + /// <value>The date of last Keeplive received.</value> + DateTime LastKeepAliveDate { get; set; } + /// <summary> - /// Gets or sets the query string. + /// Gets the query string. /// </summary> /// <value>The query string.</value> - QueryParamCollection QueryString { get; set; } + IQueryCollection QueryString { get; } /// <summary> /// Gets or sets the receive action. /// </summary> /// <value>The receive action.</value> - Func<WebSocketMessageInfo, Task> OnReceive { get; set; } + Func<WebSocketMessageInfo, Task>? OnReceive { get; set; } /// <summary> /// Gets the state. @@ -53,33 +51,18 @@ namespace MediaBrowser.Controller.Net /// Gets the remote end point. /// </summary> /// <value>The remote end point.</value> - string RemoteEndPoint { get; } + IPAddress? RemoteEndPoint { get; } /// <summary> /// Sends a message asynchronously. /// </summary> - /// <typeparam name="T"></typeparam> + /// <typeparam name="T">The type of websocket message data.</typeparam> /// <param name="message">The message.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException">message</exception> + /// <exception cref="ArgumentNullException">The message is null.</exception> Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken); - /// <summary> - /// Sends a message asynchronously. - /// </summary> - /// <param name="buffer">The buffer.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - Task SendAsync(byte[] buffer, CancellationToken cancellationToken); - - /// <summary> - /// Sends a message asynchronously. - /// </summary> - /// <param name="text">The text.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException">buffer</exception> - Task SendAsync(string text, CancellationToken cancellationToken); + Task ProcessAsync(CancellationToken cancellationToken = default); } } diff --git a/MediaBrowser.Controller/Net/IWebSocketListener.cs b/MediaBrowser.Controller/Net/IWebSocketListener.cs index e38f0e259..f1a75d518 100644 --- a/MediaBrowser.Controller/Net/IWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/IWebSocketListener.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; namespace MediaBrowser.Controller.Net { /// <summary> - ///This is an interface for listening to messages coming through a web socket connection + /// Interface for listening to messages coming through a web socket connection. /// </summary> public interface IWebSocketListener { @@ -12,6 +12,13 @@ namespace MediaBrowser.Controller.Net /// </summary> /// <param name="message">The message.</param> /// <returns>Task.</returns> - Task ProcessMessage(WebSocketMessageInfo message); + Task ProcessMessageAsync(WebSocketMessageInfo message); + + /// <summary> + /// Processes a new web socket connection. + /// </summary> + /// <param name="connection">An instance of the <see cref="IWebSocketConnection"/> interface.</param> + /// <returns>Task.</returns> + Task ProcessWebSocketConnectedAsync(IWebSocketConnection connection); } } diff --git a/MediaBrowser.Controller/Net/IWebSocketManager.cs b/MediaBrowser.Controller/Net/IWebSocketManager.cs new file mode 100644 index 000000000..bb0ae83be --- /dev/null +++ b/MediaBrowser.Controller/Net/IWebSocketManager.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace MediaBrowser.Controller.Net +{ + /// <summary> + /// Interface IHttpServer. + /// </summary> + public interface IWebSocketManager + { + /// <summary> + /// The HTTP request handler. + /// </summary> + /// <param name="context">The current HTTP context.</param> + /// <returns>The task.</returns> + Task WebSocketRequestHandler(HttpContext context); + } +} diff --git a/MediaBrowser.Controller/Net/SecurityException.cs b/MediaBrowser.Controller/Net/SecurityException.cs index 3ccecf0eb..f0d0b45a0 100644 --- a/MediaBrowser.Controller/Net/SecurityException.cs +++ b/MediaBrowser.Controller/Net/SecurityException.cs @@ -2,20 +2,36 @@ using System; namespace MediaBrowser.Controller.Net { + /// <summary> + /// The exception that is thrown when a user is authenticated, but not authorized to access a requested resource. + /// </summary> public class SecurityException : Exception { + /// <summary> + /// Initializes a new instance of the <see cref="SecurityException"/> class. + /// </summary> + public SecurityException() + : base() + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="SecurityException"/> class. + /// </summary> + /// <param name="message">The message that describes the error.</param> public SecurityException(string message) : base(message) { - } - public SecurityExceptionType SecurityExceptionType { get; set; } - } - - public enum SecurityExceptionType - { - Unauthenticated = 0, - ParentalControl = 1 + /// <summary> + /// Initializes a new instance of the <see cref="SecurityException"/> class. + /// </summary> + /// <param name="message">The message that describes the error.</param> + /// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param> + public SecurityException(string message, Exception innerException) + : base(message, innerException) + { + } } } diff --git a/MediaBrowser.Controller/Net/StaticResultOptions.cs b/MediaBrowser.Controller/Net/StaticResultOptions.cs deleted file mode 100644 index 7a179913a..000000000 --- a/MediaBrowser.Controller/Net/StaticResultOptions.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -using MediaBrowser.Model.IO; - -namespace MediaBrowser.Controller.Net -{ - public class StaticResultOptions - { - public string ContentType { get; set; } - public TimeSpan? CacheDuration { get; set; } - public DateTime? DateLastModified { get; set; } - public Func<Task<Stream>> ContentFactory { get; set; } - - public bool IsHeadRequest { get; set; } - - public IDictionary<string, string> ResponseHeaders { get; set; } - - public Action OnComplete { get; set; } - public Action OnError { get; set; } - - public string Path { get; set; } - public long? ContentLength { get; set; } - - public FileShareMode FileShare { get; set; } - - public StaticResultOptions() - { - ResponseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - FileShare = FileShareMode.Read; - } - } - - public class StaticFileResultOptions : StaticResultOptions - { - } -} diff --git a/MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs b/MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs deleted file mode 100644 index f26b764bb..000000000 --- a/MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using MediaBrowser.Model.Services; - -namespace MediaBrowser.Controller.Net -{ - /// <summary> - /// Class WebSocketConnectEventArgs - /// </summary> - public class WebSocketConnectingEventArgs : EventArgs - { - /// <summary> - /// Gets or sets the URL. - /// </summary> - /// <value>The URL.</value> - public string Url { get; set; } - /// <summary> - /// Gets or sets the endpoint. - /// </summary> - /// <value>The endpoint.</value> - public string Endpoint { get; set; } - /// <summary> - /// Gets or sets the query string. - /// </summary> - /// <value>The query string.</value> - public QueryParamCollection QueryString { get; set; } - /// <summary> - /// Gets or sets a value indicating whether [allow connection]. - /// </summary> - /// <value><c>true</c> if [allow connection]; otherwise, <c>false</c>.</value> - public bool AllowConnection { get; set; } - - public WebSocketConnectingEventArgs() - { - QueryString = new QueryParamCollection(); - AllowConnection = true; - } - } - -} diff --git a/MediaBrowser.Controller/Net/WebSocketListenerState.cs b/MediaBrowser.Controller/Net/WebSocketListenerState.cs new file mode 100644 index 000000000..70604d60a --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketListenerState.cs @@ -0,0 +1,17 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; + +namespace MediaBrowser.Controller.Net +{ + public class WebSocketListenerState + { + public DateTime DateLastSendUtc { get; set; } + + public long InitialDelayMs { get; set; } + + public long IntervalMs { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs index 5bf39cae6..6f7ebf156 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs @@ -1,9 +1,11 @@ +#nullable disable + using MediaBrowser.Model.Net; namespace MediaBrowser.Controller.Net { /// <summary> - /// Class WebSocketMessageInfo + /// Class WebSocketMessageInfo. /// </summary> public class WebSocketMessageInfo : WebSocketMessage<string> { diff --git a/MediaBrowser.Controller/Notifications/INotificationManager.cs b/MediaBrowser.Controller/Notifications/INotificationManager.cs index 44defbe0b..7caba1097 100644 --- a/MediaBrowser.Controller/Notifications/INotificationManager.cs +++ b/MediaBrowser.Controller/Notifications/INotificationManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -17,7 +19,7 @@ namespace MediaBrowser.Controller.Notifications /// <returns>Task.</returns> Task SendNotification(NotificationRequest request, CancellationToken cancellationToken); - Task SendNotification(NotificationRequest request, BaseItem relatedItem, CancellationToken cancellationToken); + Task SendNotification(NotificationRequest request, BaseItem? relatedItem, CancellationToken cancellationToken); /// <summary> /// Adds the parts. diff --git a/MediaBrowser.Controller/Notifications/INotificationService.cs b/MediaBrowser.Controller/Notifications/INotificationService.cs index 8c6019923..535c08795 100644 --- a/MediaBrowser.Controller/Notifications/INotificationService.cs +++ b/MediaBrowser.Controller/Notifications/INotificationService.cs @@ -1,6 +1,10 @@ +#nullable disable + +#pragma warning disable CS1591 + using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; +using Jellyfin.Data.Entities; namespace MediaBrowser.Controller.Notifications { diff --git a/MediaBrowser.Controller/Notifications/INotificationTypeFactory.cs b/MediaBrowser.Controller/Notifications/INotificationTypeFactory.cs index 9f1d2841d..52a3e120b 100644 --- a/MediaBrowser.Controller/Notifications/INotificationTypeFactory.cs +++ b/MediaBrowser.Controller/Notifications/INotificationTypeFactory.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using MediaBrowser.Model.Notifications; diff --git a/MediaBrowser.Controller/Notifications/UserNotification.cs b/MediaBrowser.Controller/Notifications/UserNotification.cs index 3f46468b3..4be0e09ae 100644 --- a/MediaBrowser.Controller/Notifications/UserNotification.cs +++ b/MediaBrowser.Controller/Notifications/UserNotification.cs @@ -1,5 +1,9 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; -using MediaBrowser.Controller.Entities; +using Jellyfin.Data.Entities; using MediaBrowser.Model.Notifications; namespace MediaBrowser.Controller.Notifications diff --git a/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs b/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs deleted file mode 100644 index 4424e044b..000000000 --- a/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Controller.Persistence -{ - /// <summary> - /// Interface IDisplayPreferencesRepository - /// </summary> - public interface IDisplayPreferencesRepository : IRepository - { - /// <summary> - /// Saves display preferences for an item - /// </summary> - /// <param name="displayPreferences">The display preferences.</param> - /// <param name="userId">The user id.</param> - /// <param name="client">The client.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, - CancellationToken cancellationToken); - - /// <summary> - /// Saves all display preferences for a user - /// </summary> - /// <param name="displayPreferences">The display preferences.</param> - /// <param name="userId">The user id.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - void SaveAllDisplayPreferences(IEnumerable<DisplayPreferences> displayPreferences, Guid userId, - CancellationToken cancellationToken); - /// <summary> - /// Gets the display preferences. - /// </summary> - /// <param name="displayPreferencesId">The display preferences id.</param> - /// <param name="userId">The user id.</param> - /// <param name="client">The client.</param> - /// <returns>Task{DisplayPreferences}.</returns> - DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client); - - /// <summary> - /// Gets all display preferences for the given user. - /// </summary> - /// <param name="userId">The user id.</param> - /// <returns>Task{DisplayPreferences}.</returns> - IEnumerable<DisplayPreferences> GetAllDisplayPreferences(Guid userId); - } -} diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 47e0f3453..a084f9196 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading; @@ -9,12 +13,12 @@ using MediaBrowser.Model.Querying; namespace MediaBrowser.Controller.Persistence { /// <summary> - /// Provides an interface to implement an Item repository + /// Provides an interface to implement an Item repository. /// </summary> public interface IItemRepository : IRepository { /// <summary> - /// Saves an item + /// Saves an item. /// </summary> /// <param name="item">The item.</param> /// <param name="cancellationToken">The cancellation token.</param> @@ -24,8 +28,7 @@ namespace MediaBrowser.Controller.Persistence /// Deletes the item. /// </summary> /// <param name="id">The identifier.</param> - /// <param name="cancellationToken">The cancellation token.</param> - void DeleteItem(Guid id, CancellationToken cancellationToken); + void DeleteItem(Guid id); /// <summary> /// Saves the items. @@ -44,24 +47,26 @@ namespace MediaBrowser.Controller.Persistence BaseItem RetrieveItem(Guid id); /// <summary> - /// Gets chapters for an item + /// Gets chapters for an item. /// </summary> - /// <param name="id"></param> - /// <returns></returns> - List<ChapterInfo> GetChapters(BaseItem id); + /// <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 + /// Gets a single chapter for an item. /// </summary> - /// <param name="id"></param> - /// <param name="index"></param> - /// <returns></returns> - ChapterInfo GetChapter(BaseItem id, int index); + /// <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> - void SaveChapters(Guid id, List<ChapterInfo> chapters); + /// <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. @@ -79,11 +84,27 @@ namespace MediaBrowser.Controller.Persistence void SaveMediaStreams(Guid id, List<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 item ids. /// </summary> /// <param name="query">The query.</param> /// <returns>IEnumerable<Guid>.</returns> QueryResult<Guid> GetItemIds(InternalItemsQuery query); + /// <summary> /// Gets the items. /// </summary> @@ -136,22 +157,28 @@ namespace MediaBrowser.Controller.Persistence /// <summary> /// Updates the inherited values. /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - void UpdateInheritedValues(CancellationToken cancellationToken); + void UpdateInheritedValues(); int GetCount(InternalItemsQuery query); QueryResult<(BaseItem, ItemCounts)> GetGenres(InternalItemsQuery query); + QueryResult<(BaseItem, ItemCounts)> GetMusicGenres(InternalItemsQuery query); + QueryResult<(BaseItem, ItemCounts)> GetStudios(InternalItemsQuery query); + QueryResult<(BaseItem, ItemCounts)> GetArtists(InternalItemsQuery query); + QueryResult<(BaseItem, ItemCounts)> GetAlbumArtists(InternalItemsQuery query); + QueryResult<(BaseItem, ItemCounts)> GetAllArtists(InternalItemsQuery query); List<string> GetMusicGenreNames(); + List<string> GetStudioNames(); + List<string> GetGenreNames(); + List<string> GetAllArtistNames(); } } - diff --git a/MediaBrowser.Controller/Persistence/IRepository.cs b/MediaBrowser.Controller/Persistence/IRepository.cs index 56bf1dd5a..42f285076 100644 --- a/MediaBrowser.Controller/Persistence/IRepository.cs +++ b/MediaBrowser.Controller/Persistence/IRepository.cs @@ -3,12 +3,12 @@ using System; namespace MediaBrowser.Controller.Persistence { /// <summary> - /// Provides a base interface for all the repository interfaces + /// Provides a base interface for all the repository interfaces. /// </summary> public interface IRepository : IDisposable { /// <summary> - /// Gets the name of the repository + /// Gets the name of the repository. /// </summary> /// <value>The name.</value> string Name { get; } diff --git a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs index a4bdf60d7..c43acfb6d 100644 --- a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs +++ b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Collections.Generic; using System.Threading; using MediaBrowser.Controller.Entities; @@ -5,7 +7,7 @@ using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Persistence { /// <summary> - /// Provides an interface to implement a UserData repository + /// Provides an interface to implement a UserData repository. /// </summary> public interface IUserDataRepository : IRepository { @@ -16,7 +18,6 @@ namespace MediaBrowser.Controller.Persistence /// <param name="key">The key.</param> /// <param name="userData">The user data.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken); /// <summary> @@ -24,26 +25,30 @@ namespace MediaBrowser.Controller.Persistence /// </summary> /// <param name="userId">The user id.</param> /// <param name="key">The key.</param> - /// <returns>Task{UserItemData}.</returns> + /// <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 + /// Return all user data associated with the given user. /// </summary> - /// <param name="userId"></param> - /// <returns></returns> + /// <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 + /// Save all user data associated with the given user. /// </summary> - /// <param name="userId"></param> - /// <param name="userData"></param> - /// <param name="cancellationToken"></param> - /// <returns></returns> + /// <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/Persistence/IUserRepository.cs b/MediaBrowser.Controller/Persistence/IUserRepository.cs deleted file mode 100644 index cd23e5223..000000000 --- a/MediaBrowser.Controller/Persistence/IUserRepository.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using MediaBrowser.Controller.Entities; - -namespace MediaBrowser.Controller.Persistence -{ - /// <summary> - /// Provides an interface to implement a User repository - /// </summary> - public interface IUserRepository : IRepository - { - /// <summary> - /// Deletes the user. - /// </summary> - /// <param name="user">The user.</param> - /// <returns>Task.</returns> - void DeleteUser(User user); - - /// <summary> - /// Retrieves all users. - /// </summary> - /// <returns>IEnumerable{User}.</returns> - List<User> RetrieveAllUsers(); - - void CreateUser(User user); - void UpdateUser(User user); - } -} diff --git a/MediaBrowser.Controller/Persistence/MediaAttachmentQuery.cs b/MediaBrowser.Controller/Persistence/MediaAttachmentQuery.cs new file mode 100644 index 000000000..e07e96f73 --- /dev/null +++ b/MediaBrowser.Controller/Persistence/MediaAttachmentQuery.cs @@ -0,0 +1,21 @@ +#pragma warning disable CS1591 + +using System; + +namespace MediaBrowser.Controller.Persistence +{ + public class MediaAttachmentQuery + { + /// <summary> + /// Gets or sets the index. + /// </summary> + /// <value>The index.</value> + public int? Index { get; set; } + + /// <summary> + /// Gets or sets the item identifier. + /// </summary> + /// <value>The item identifier.</value> + public Guid ItemId { get; set; } + } +} diff --git a/MediaBrowser.Controller/Persistence/MediaStreamQuery.cs b/MediaBrowser.Controller/Persistence/MediaStreamQuery.cs index 7dc563b3a..f9295c8fd 100644 --- a/MediaBrowser.Controller/Persistence/MediaStreamQuery.cs +++ b/MediaBrowser.Controller/Persistence/MediaStreamQuery.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index 5001f6842..f6c592070 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -29,7 +31,7 @@ namespace MediaBrowser.Controller.Playlists /// <param name="itemIds">The item ids.</param> /// <param name="userId">The user identifier.</param> /// <returns>Task.</returns> - void AddToPlaylist(string playlistId, IEnumerable<Guid> itemIds, Guid userId); + Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId); /// <summary> /// Removes from playlist. @@ -37,7 +39,7 @@ namespace MediaBrowser.Controller.Playlists /// <param name="playlistId">The playlist identifier.</param> /// <param name="entryIds">The entry ids.</param> /// <returns>Task.</returns> - void RemoveFromPlaylist(string playlistId, IEnumerable<string> entryIds); + Task RemoveFromPlaylistAsync(string playlistId, IEnumerable<string> entryIds); /// <summary> /// Gets the playlists folder. @@ -53,6 +55,6 @@ namespace MediaBrowser.Controller.Playlists /// <param name="entryId">The entry identifier.</param> /// <param name="newIndex">The new index.</param> /// <returns>Task.</returns> - void MoveItem(string playlistId, string entryId, int newIndex); + Task MoveItemAsync(string playlistId, string entryId, int newIndex); } } diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index e83260725..5e671a725 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -1,47 +1,49 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; +using System.Globalization; +using System.IO; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Playlists { public class Playlist : Folder, IHasShares { - public static string[] SupportedExtensions = - { - ".m3u", - ".m3u8", - ".pls", - ".wpl", - ".zpl" - }; - - public Guid OwnerUserId { get; set; } - - public Share[] Shares { get; set; } + public static readonly IReadOnlyList<string> SupportedExtensions = new[] + { + ".m3u", + ".m3u8", + ".pls", + ".wpl", + ".zpl" + }; public Playlist() { Shares = Array.Empty<Share>(); } - [IgnoreDataMember] - public bool IsFile => IsPlaylistFile(Path); + public Guid OwnerUserId { get; set; } - public static bool IsPlaylistFile(string path) - { - return System.IO.Path.HasExtension(path); - } + public Share[] Shares { get; set; } + + [JsonIgnore] + public bool IsFile => IsPlaylistFile(Path); - [IgnoreDataMember] + [JsonIgnore] public override string ContainingFolderPath { get @@ -57,21 +59,56 @@ namespace MediaBrowser.Controller.Playlists } } - [IgnoreDataMember] + [JsonIgnore] protected override bool FilterLinkedChildrenPerUser => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => string.Equals(MediaType, "Video", StringComparison.OrdinalIgnoreCase); - [IgnoreDataMember] + [JsonIgnore] public override bool AlwaysScanInternalMetadataPath => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsCumulativeRunTimeTicks => true; + [JsonIgnore] + public override bool IsPreSorted => true; + + public string PlaylistMediaType { get; set; } + + [JsonIgnore] + public override string MediaType => PlaylistMediaType; + + [JsonIgnore] + private bool IsSharedItem + { + get + { + var path = Path; + + if (string.IsNullOrEmpty(path)) + { + return false; + } + + return FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, path); + } + } + + public static bool IsPlaylistFile(string path) + { + // The path will sometimes be a directory and "Path.HasExtension" returns true if the name contains a '.' (dot). + return System.IO.Path.HasExtension(path) && !Directory.Exists(path); + } + + public void SetMediaType(string value) + { + PlaylistMediaType = value; + } + public override double GetDefaultPrimaryImageAspectRatio() { return 1; @@ -93,7 +130,7 @@ namespace MediaBrowser.Controller.Playlists return new List<BaseItem>(); } - protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { return Task.CompletedTask; } @@ -120,10 +157,7 @@ namespace MediaBrowser.Controller.Playlists private List<BaseItem> GetPlayableItems(User user, InternalItemsQuery query) { - if (query == null) - { - query = new InternalItemsQuery(user); - } + query ??= new InternalItemsQuery(user); query.IsFolder = false; @@ -155,9 +189,9 @@ namespace MediaBrowser.Controller.Playlists return LibraryManager.GetItemList(new InternalItemsQuery(user) { Recursive = true, - IncludeItemTypes = new[] { typeof(Audio).Name }, + IncludeItemTypes = new[] { nameof(Audio) }, GenreIds = new[] { musicGenre.Id }, - OrderBy = new[] { ItemSortBy.AlbumArtist, ItemSortBy.Album, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(), + OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) }, DtoOptions = options }); } @@ -167,9 +201,9 @@ namespace MediaBrowser.Controller.Playlists return LibraryManager.GetItemList(new InternalItemsQuery(user) { Recursive = true, - IncludeItemTypes = new[] { typeof(Audio).Name }, + IncludeItemTypes = new[] { nameof(Audio) }, ArtistIds = new[] { musicArtist.Id }, - OrderBy = new[] { ItemSortBy.AlbumArtist, ItemSortBy.Album, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(), + OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) }, DtoOptions = options }); } @@ -180,7 +214,7 @@ namespace MediaBrowser.Controller.Playlists { Recursive = true, IsFolder = false, - OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(), + OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, MediaTypes = new[] { mediaType }, EnableTotalRecordCount = false, DtoOptions = options @@ -192,35 +226,6 @@ namespace MediaBrowser.Controller.Playlists return new[] { item }; } - [IgnoreDataMember] - public override bool IsPreSorted => true; - - public string PlaylistMediaType { get; set; } - - [IgnoreDataMember] - public override string MediaType => PlaylistMediaType; - - public void SetMediaType(string value) - { - PlaylistMediaType = value; - } - - [IgnoreDataMember] - private bool IsSharedItem - { - get - { - var path = Path; - - if (string.IsNullOrEmpty(path)) - { - return false; - } - - return FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, path); - } - } - public override bool IsVisible(User user) { if (!IsSharedItem) @@ -239,16 +244,8 @@ namespace MediaBrowser.Controller.Playlists return base.IsVisible(user); } - var userId = user.Id.ToString("N"); - foreach (var share in shares) - { - if (string.Equals(share.UserId, userId, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; + var userId = user.Id.ToString("N", CultureInfo.InvariantCulture); + return shares.Any(share => string.Equals(share.UserId, userId, StringComparison.OrdinalIgnoreCase)); } public override bool IsVisibleStandalone(User user) diff --git a/MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs b/MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs deleted file mode 100644 index 5deb165f6..000000000 --- a/MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.IO; -using System.Reflection; - -namespace MediaBrowser.Controller.Plugins -{ - public interface ILocalizablePlugin - { - Stream GetDictionary(string culture); - } - - public static class LocalizablePluginHelper - { - public static Stream GetDictionary(Assembly assembly, string manifestPrefix, string culture) - { - // Find all dictionaries using GetManifestResourceNames, start start with the prefix - // Return the one for the culture if exists, otherwise return the default - return null; - } - } -} diff --git a/MediaBrowser.Controller/Plugins/IPluginConfigurationPage.cs b/MediaBrowser.Controller/Plugins/IPluginConfigurationPage.cs deleted file mode 100644 index c156da924..000000000 --- a/MediaBrowser.Controller/Plugins/IPluginConfigurationPage.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.IO; -using MediaBrowser.Common.Plugins; - -namespace MediaBrowser.Controller.Plugins -{ - /// <summary> - /// Interface IConfigurationPage - /// </summary> - public interface IPluginConfigurationPage - { - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - string Name { get; } - - /// <summary> - /// Gets the type of the configuration page. - /// </summary> - /// <value>The type of the configuration page.</value> - ConfigurationPageType ConfigurationPageType { get; } - - /// <summary> - /// Gets the plugin. - /// </summary> - /// <value>The plugin.</value> - IPlugin Plugin { get; } - - /// <summary> - /// Gets the HTML stream. - /// </summary> - /// <returns>Stream.</returns> - Stream GetHtmlStream(); - } - - /// <summary> - /// Enum ConfigurationPageType - /// </summary> - public enum ConfigurationPageType - { - /// <summary> - /// The plugin configuration - /// </summary> - PluginConfiguration, - /// <summary> - /// The none - /// </summary> - None - } -} diff --git a/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs b/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs new file mode 100644 index 000000000..2b831103a --- /dev/null +++ b/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs @@ -0,0 +1,9 @@ +namespace MediaBrowser.Controller.Plugins +{ + /// <summary> + /// Indicates that a <see cref="IServerEntryPoint"/> should be invoked as a pre-startup task. + /// </summary> + public interface IRunBeforeStartup + { + } +} diff --git a/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs b/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs index e57929989..6024661e1 100644 --- a/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs +++ b/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs @@ -4,18 +4,17 @@ using System.Threading.Tasks; namespace MediaBrowser.Controller.Plugins { /// <summary> - /// Interface IServerEntryPoint + /// Represents an entry point for a module in the application. This interface is scanned for automatically and + /// provides a hook to initialize the module at application start. + /// The entry point can additionally be flagged as a pre-startup task by implementing the + /// <see cref="IRunBeforeStartup"/> interface. /// </summary> public interface IServerEntryPoint : IDisposable { /// <summary> - /// Runs this instance. + /// Run the initialization for this module. This method is invoked at application start. /// </summary> + /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> Task RunAsync(); } - - public interface IRunBeforeStartup - { - - } } diff --git a/MediaBrowser.Controller/Properties/AssemblyInfo.cs b/MediaBrowser.Controller/Properties/AssemblyInfo.cs index 007a1d739..60e792309 100644 --- a/MediaBrowser.Controller/Properties/AssemblyInfo.cs +++ b/MediaBrowser.Controller/Properties/AssemblyInfo.cs @@ -9,8 +9,8 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyProduct("Jellyfin Server")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/MediaBrowser.Controller/Providers/AlbumInfo.cs b/MediaBrowser.Controller/Providers/AlbumInfo.cs index b0b443fc0..aefa520e7 100644 --- a/MediaBrowser.Controller/Providers/AlbumInfo.cs +++ b/MediaBrowser.Controller/Providers/AlbumInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA1002, CA2227, CS1591 + using System; using System.Collections.Generic; @@ -5,24 +7,25 @@ namespace MediaBrowser.Controller.Providers { public class AlbumInfo : ItemLookupInfo { + public AlbumInfo() + { + ArtistProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + SongInfos = new List<SongInfo>(); + AlbumArtists = Array.Empty<string>(); + } + /// <summary> /// Gets or sets the album artist. /// </summary> /// <value>The album artist.</value> - public string[] AlbumArtists { get; set; } + public IReadOnlyList<string> AlbumArtists { get; set; } /// <summary> /// Gets or sets the artist provider ids. /// </summary> /// <value>The artist provider ids.</value> public Dictionary<string, string> ArtistProviderIds { get; set; } - public List<SongInfo> SongInfos { get; set; } - public AlbumInfo() - { - ArtistProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - SongInfos = new List<SongInfo>(); - AlbumArtists = Array.Empty<string>(); - } + public List<SongInfo> SongInfos { get; set; } } } diff --git a/MediaBrowser.Controller/Providers/ArtistInfo.cs b/MediaBrowser.Controller/Providers/ArtistInfo.cs index 08bf3982b..4854d1a5f 100644 --- a/MediaBrowser.Controller/Providers/ArtistInfo.cs +++ b/MediaBrowser.Controller/Providers/ArtistInfo.cs @@ -1,14 +1,16 @@ +#pragma warning disable CA1002, CA2227, CS1591 + using System.Collections.Generic; namespace MediaBrowser.Controller.Providers { public class ArtistInfo : ItemLookupInfo { - public List<SongInfo> SongInfos { get; set; } - public ArtistInfo() { SongInfos = new List<SongInfo>(); } + + public List<SongInfo> SongInfos { get; set; } } } diff --git a/MediaBrowser.Controller/Providers/BookInfo.cs b/MediaBrowser.Controller/Providers/BookInfo.cs index 03a6737c5..3055c5d87 100644 --- a/MediaBrowser.Controller/Providers/BookInfo.cs +++ b/MediaBrowser.Controller/Providers/BookInfo.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Providers { public class BookInfo : ItemLookupInfo diff --git a/MediaBrowser.Controller/Providers/BoxSetInfo.cs b/MediaBrowser.Controller/Providers/BoxSetInfo.cs index 4cbe2d6ef..f43ea6717 100644 --- a/MediaBrowser.Controller/Providers/BoxSetInfo.cs +++ b/MediaBrowser.Controller/Providers/BoxSetInfo.cs @@ -1,7 +1,8 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Providers { public class BoxSetInfo : ItemLookupInfo { - } } diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs index 133e7c115..b31270270 100644 --- a/MediaBrowser.Controller/Providers/DirectoryService.cs +++ b/MediaBrowser.Controller/Providers/DirectoryService.cs @@ -1,94 +1,82 @@ +#pragma warning disable CS1591 + using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using MediaBrowser.Model.IO; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Providers { public class DirectoryService : IDirectoryService { - private readonly ILogger _logger; private readonly IFileSystem _fileSystem; - private readonly Dictionary<string, FileSystemMetadata[]> _cache = new Dictionary<string, FileSystemMetadata[]>(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary<string, FileSystemMetadata[]> _cache = new (StringComparer.Ordinal); - private readonly Dictionary<string, FileSystemMetadata> _fileCache = new Dictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary<string, FileSystemMetadata> _fileCache = new (StringComparer.Ordinal); - private readonly Dictionary<string, List<string>> _filePathCache = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary<string, List<string>> _filePathCache = new (StringComparer.Ordinal); - public DirectoryService(ILogger logger, IFileSystem fileSystem) + public DirectoryService(IFileSystem fileSystem) { - _logger = logger; _fileSystem = fileSystem; } public FileSystemMetadata[] GetFileSystemEntries(string path) { - if (!_cache.TryGetValue(path, out FileSystemMetadata[] entries)) - { - //_logger.LogDebug("Getting files for " + path); - - entries = _fileSystem.GetFileSystemEntries(path).ToArray(); - - //_cache.TryAdd(path, entries); - _cache[path] = entries; - } - - return entries; + return _cache.GetOrAdd(path, (p, fileSystem) => fileSystem.GetFileSystemEntries(p).ToArray(), _fileSystem); } public List<FileSystemMetadata> GetFiles(string path) { var list = new List<FileSystemMetadata>(); var items = GetFileSystemEntries(path); - foreach (var item in items) + for (var i = 0; i < items.Length; i++) { + var item = items[i]; if (!item.IsDirectory) { list.Add(item); } } + return list; } - public FileSystemMetadata GetFile(string path) + public FileSystemMetadata? GetFile(string path) { - if (!_fileCache.TryGetValue(path, out FileSystemMetadata file)) + if (!_fileCache.TryGetValue(path, out var result)) { - file = _fileSystem.GetFileInfo(path); - - if (file != null && file.Exists) - { - //_fileCache.TryAdd(path, file); - _fileCache[path] = file; - } - else + var file = _fileSystem.GetFileInfo(path); + if (file.Exists) { - return null; + result = file; + _fileCache.TryAdd(path, result); } } - return file; - //return _fileSystem.GetFileInfo(path); + return result; } - public List<string> GetFilePaths(string path) - { - return GetFilePaths(path, false); - } + public IReadOnlyList<string> GetFilePaths(string path) + => GetFilePaths(path, false); - public List<string> GetFilePaths(string path, bool clearCache) + public IReadOnlyList<string> GetFilePaths(string path, bool clearCache, bool sort = false) { - if (clearCache || !_filePathCache.TryGetValue(path, out List<string> result)) + if (clearCache) { - result = _fileSystem.GetFilePaths(path).ToList(); + _filePathCache.TryRemove(path, out _); + } - _filePathCache[path] = result; + var filePaths = _filePathCache.GetOrAdd(path, (p, fileSystem) => fileSystem.GetFilePaths(p).ToList(), _fileSystem); + + if (sort) + { + filePaths.Sort(); } - return result; + return filePaths; } - } } diff --git a/MediaBrowser.Controller/Providers/DynamicImageInfo.cs b/MediaBrowser.Controller/Providers/DynamicImageInfo.cs deleted file mode 100644 index 0791783c6..000000000 --- a/MediaBrowser.Controller/Providers/DynamicImageInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Controller.Providers -{ - public class DynamicImageInfo - { - public string ImageId { get; set; } - public ImageType Type { get; set; } - } -} diff --git a/MediaBrowser.Controller/Providers/DynamicImageResponse.cs b/MediaBrowser.Controller/Providers/DynamicImageResponse.cs index 11c7ccbe5..66fddb0e3 100644 --- a/MediaBrowser.Controller/Providers/DynamicImageResponse.cs +++ b/MediaBrowser.Controller/Providers/DynamicImageResponse.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.IO; using MediaBrowser.Model.Drawing; @@ -8,9 +12,13 @@ namespace MediaBrowser.Controller.Providers public class DynamicImageResponse { public string Path { get; set; } + public MediaProtocol Protocol { get; set; } + public Stream Stream { get; set; } + public ImageFormat Format { get; set; } + public bool HasImage { get; set; } public void SetFormatFromMimeType(string mimeType) diff --git a/MediaBrowser.Controller/Providers/EpisodeInfo.cs b/MediaBrowser.Controller/Providers/EpisodeInfo.cs index 6ecf4edc5..b59a03738 100644 --- a/MediaBrowser.Controller/Providers/EpisodeInfo.cs +++ b/MediaBrowser.Controller/Providers/EpisodeInfo.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CA2227, CS1591 + using System; using System.Collections.Generic; @@ -5,16 +9,17 @@ namespace MediaBrowser.Controller.Providers { public class EpisodeInfo : ItemLookupInfo { + public EpisodeInfo() + { + SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + public Dictionary<string, string> SeriesProviderIds { get; set; } public int? IndexNumberEnd { get; set; } public bool IsMissingEpisode { get; set; } - public string SeriesDisplayOrder { get; set; } - public EpisodeInfo() - { - SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } + public string SeriesDisplayOrder { get; set; } } } diff --git a/MediaBrowser.Controller/Providers/ExtraInfo.cs b/MediaBrowser.Controller/Providers/ExtraInfo.cs deleted file mode 100644 index 413ff2e78..000000000 --- a/MediaBrowser.Controller/Providers/ExtraInfo.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Controller.Providers -{ - public class ExtraInfo - { - public string Path { get; set; } - - public LocationType LocationType { get; set; } - - public bool IsDownloadable { get; set; } - - public ExtraType ExtraType { get; set; } - } -} diff --git a/MediaBrowser.Controller/Providers/ExtraSource.cs b/MediaBrowser.Controller/Providers/ExtraSource.cs deleted file mode 100644 index 46b246fbc..000000000 --- a/MediaBrowser.Controller/Providers/ExtraSource.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MediaBrowser.Controller.Providers -{ - public enum ExtraSource - { - Local = 1, - Metadata = 2, - Remote = 3 - } -} diff --git a/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs b/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs index 2d5a0b364..32a9cbef2 100644 --- a/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs +++ b/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; @@ -13,7 +15,7 @@ namespace MediaBrowser.Controller.Providers where TItemType : BaseItem { /// <summary> - /// Fetches the asynchronous. + /// Fetches the metadata asynchronously. /// </summary> /// <param name="item">The item.</param> /// <param name="options">The options.</param> diff --git a/MediaBrowser.Controller/Providers/IDirectoryService.cs b/MediaBrowser.Controller/Providers/IDirectoryService.cs index 8059d2bd1..48d627691 100644 --- a/MediaBrowser.Controller/Providers/IDirectoryService.cs +++ b/MediaBrowser.Controller/Providers/IDirectoryService.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA1002, CA1819, CS1591 + using System.Collections.Generic; using MediaBrowser.Model.IO; @@ -6,10 +8,13 @@ namespace MediaBrowser.Controller.Providers public interface IDirectoryService { FileSystemMetadata[] GetFileSystemEntries(string path); + List<FileSystemMetadata> GetFiles(string path); - FileSystemMetadata GetFile(string path); - List<string> GetFilePaths(string path); - List<string> GetFilePaths(string path, bool clearCache); + FileSystemMetadata? GetFile(string path); + + IReadOnlyList<string> GetFilePaths(string path); + + IReadOnlyList<string> GetFilePaths(string path, bool clearCache, bool sort = false); } } diff --git a/MediaBrowser.Controller/Providers/IDynamicImageProvider.cs b/MediaBrowser.Controller/Providers/IDynamicImageProvider.cs index dec327d66..ab66462fa 100644 --- a/MediaBrowser.Controller/Providers/IDynamicImageProvider.cs +++ b/MediaBrowser.Controller/Providers/IDynamicImageProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.Controller/Providers/IExternalId.cs b/MediaBrowser.Controller/Providers/IExternalId.cs index d7e337bda..0d847520d 100644 --- a/MediaBrowser.Controller/Providers/IExternalId.cs +++ b/MediaBrowser.Controller/Providers/IExternalId.cs @@ -1,15 +1,45 @@ using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; namespace MediaBrowser.Controller.Providers { + /// <summary> + /// Represents an identifier for an external provider. + /// </summary> public interface IExternalId { - string Name { get; } + /// <summary> + /// Gets the display name of the provider associated with this ID type. + /// </summary> + string ProviderName { get; } + /// <summary> + /// Gets the unique key to distinguish this provider/type pair. This should be unique across providers. + /// </summary> + // TODO: This property is not actually unique across the concrete types at the moment. It should be updated to be unique. string Key { get; } - string UrlFormatString { get; } + /// <summary> + /// Gets the specific media type for this id. This is used to distinguish between the different + /// external id types for providers with multiple ids. + /// A null value indicates there is no specific media type associated with the external id, or this is the + /// default id for the external provider so there is no need to specify a type. + /// </summary> + /// <remarks> + /// This can be used along with the <see cref="ProviderName"/> to localize the external id on the client. + /// </remarks> + ExternalIdMediaType? Type { get; } + /// <summary> + /// Gets the URL format string for this id. + /// </summary> + string? UrlFormatString { get; } + + /// <summary> + /// Determines whether this id supports a given item type. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>True if this item is supported, otherwise false.</returns> bool Supports(IHasProviderIds item); } } diff --git a/MediaBrowser.Controller/Providers/IExtrasProvider.cs b/MediaBrowser.Controller/Providers/IExtrasProvider.cs deleted file mode 100644 index fa31635cb..000000000 --- a/MediaBrowser.Controller/Providers/IExtrasProvider.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MediaBrowser.Controller.Entities; - -namespace MediaBrowser.Controller.Providers -{ - public interface IExtrasProvider - { - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - string Name { get; } - - /// <summary> - /// Supportses the specified item. - /// </summary> - /// <param name="item">The item.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> - bool Supports(BaseItem item); - } -} diff --git a/MediaBrowser.Controller/Providers/IForcedProvider.cs b/MediaBrowser.Controller/Providers/IForcedProvider.cs index 35fa29d94..5ae4a56ef 100644 --- a/MediaBrowser.Controller/Providers/IForcedProvider.cs +++ b/MediaBrowser.Controller/Providers/IForcedProvider.cs @@ -1,7 +1,7 @@ namespace MediaBrowser.Controller.Providers { /// <summary> - /// This is a marker interface that will cause a provider to run even if IsLocked=true + /// This is a marker interface that will cause a provider to run even if an item is locked from changes. /// </summary> public interface IForcedProvider { diff --git a/MediaBrowser.Controller/Providers/IHasItemChangeMonitor.cs b/MediaBrowser.Controller/Providers/IHasItemChangeMonitor.cs index 68acb3910..a0e20e312 100644 --- a/MediaBrowser.Controller/Providers/IHasItemChangeMonitor.cs +++ b/MediaBrowser.Controller/Providers/IHasItemChangeMonitor.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Providers diff --git a/MediaBrowser.Controller/Providers/IHasLookupInfo.cs b/MediaBrowser.Controller/Providers/IHasLookupInfo.cs index 4c0c38442..42cb52371 100644 --- a/MediaBrowser.Controller/Providers/IHasLookupInfo.cs +++ b/MediaBrowser.Controller/Providers/IHasLookupInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Providers { public interface IHasLookupInfo<out TLookupInfoType> diff --git a/MediaBrowser.Controller/Providers/IHasOrder.cs b/MediaBrowser.Controller/Providers/IHasOrder.cs index a3db61225..9fde0e695 100644 --- a/MediaBrowser.Controller/Providers/IHasOrder.cs +++ b/MediaBrowser.Controller/Providers/IHasOrder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Providers { public interface IHasOrder diff --git a/MediaBrowser.Controller/Providers/IImageEnhancer.cs b/MediaBrowser.Controller/Providers/IImageEnhancer.cs deleted file mode 100644 index c27c00ca2..000000000 --- a/MediaBrowser.Controller/Providers/IImageEnhancer.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Drawing; -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Controller.Providers -{ - public interface IImageEnhancer - { - /// <summary> - /// Return true only if the given image for the given item will be enhanced by this enhancer. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="imageType">Type of the image.</param> - /// <returns><c>true</c> if this enhancer will enhance the supplied image for the supplied item, <c>false</c> otherwise</returns> - bool Supports(BaseItem item, ImageType imageType); - - /// <summary> - /// Gets the priority or order in which this enhancer should be run. - /// </summary> - /// <value>The priority.</value> - MetadataProviderPriority Priority { get; } - - /// <summary> - /// Return a key incorporating all configuration information related to this item - /// </summary> - /// <param name="item">The item.</param> - /// <param name="imageType">Type of the image.</param> - /// <returns>Cache key relating to the current state of this item and configuration</returns> - string GetConfigurationCacheKey(BaseItem item, ImageType imageType); - - /// <summary> - /// Gets the size of the enhanced image. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="imageType">Type of the image.</param> - /// <param name="imageIndex">Index of the image.</param> - /// <param name="originalImageSize">Size of the original image.</param> - /// <returns>ImageSize.</returns> - ImageDimensions GetEnhancedImageSize(BaseItem item, ImageType imageType, int imageIndex, ImageDimensions originalImageSize); - - EnhancedImageInfo GetEnhancedImageInfo(BaseItem item, string inputFile, ImageType imageType, int imageIndex); - - /// <summary> - /// Enhances the image async. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="inputFile">The input file.</param> - /// <param name="outputFile">The output file.</param> - /// <param name="imageType">Type of the image.</param> - /// <param name="imageIndex">Index of the image.</param> - /// <returns>Task{Image}.</returns> - /// <exception cref="System.ArgumentNullException"></exception> - Task EnhanceImageAsync(BaseItem item, string inputFile, string outputFile, ImageType imageType, int imageIndex); - } - - public class EnhancedImageInfo - { - public bool RequiresTransparency { get; set; } - } -} diff --git a/MediaBrowser.Controller/Providers/IImageProvider.cs b/MediaBrowser.Controller/Providers/IImageProvider.cs index 2df3d5ff8..29ab323f8 100644 --- a/MediaBrowser.Controller/Providers/IImageProvider.cs +++ b/MediaBrowser.Controller/Providers/IImageProvider.cs @@ -3,7 +3,7 @@ using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Providers { /// <summary> - /// Interface IImageProvider + /// Interface IImageProvider. /// </summary> public interface IImageProvider { @@ -14,10 +14,10 @@ namespace MediaBrowser.Controller.Providers string Name { get; } /// <summary> - /// Supportses the specified item. + /// Supports the specified item. /// </summary> /// <param name="item">The item.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> + /// <returns><c>true</c> if the provider supports the item.</returns> bool Supports(BaseItem item); } } diff --git a/MediaBrowser.Controller/Providers/ILocalImageFileProvider.cs b/MediaBrowser.Controller/Providers/ILocalImageFileProvider.cs deleted file mode 100644 index 72bc56390..000000000 --- a/MediaBrowser.Controller/Providers/ILocalImageFileProvider.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; -using MediaBrowser.Controller.Entities; - -namespace MediaBrowser.Controller.Providers -{ - public interface ILocalImageFileProvider : ILocalImageProvider - { - List<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService); - } -} diff --git a/MediaBrowser.Controller/Providers/ILocalImageProvider.cs b/MediaBrowser.Controller/Providers/ILocalImageProvider.cs index 09aaab3de..f78bd6ddf 100644 --- a/MediaBrowser.Controller/Providers/ILocalImageProvider.cs +++ b/MediaBrowser.Controller/Providers/ILocalImageProvider.cs @@ -1,9 +1,15 @@ +#pragma warning disable CS1591 + +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; + namespace MediaBrowser.Controller.Providers { /// <summary> - /// This is just a marker interface + /// This is just a marker interface. /// </summary> public interface ILocalImageProvider : IImageProvider { + IEnumerable<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService); } } diff --git a/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs b/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs index 2a2c379f6..e771c881d 100644 --- a/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs +++ b/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; @@ -17,8 +19,9 @@ namespace MediaBrowser.Controller.Providers /// <param name="info">The information.</param> /// <param name="directoryService">The directory service.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{MetadataResult{`0}}.</returns> - Task<MetadataResult<TItemType>> GetMetadata(ItemInfo info, + /// <returns>Task{MetadataResult{0}}.</returns> + Task<MetadataResult<TItemType>> GetMetadata( + ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken); } diff --git a/MediaBrowser.Controller/Providers/IMetadataProvider.cs b/MediaBrowser.Controller/Providers/IMetadataProvider.cs index 3e595ff93..1a87e0625 100644 --- a/MediaBrowser.Controller/Providers/IMetadataProvider.cs +++ b/MediaBrowser.Controller/Providers/IMetadataProvider.cs @@ -1,9 +1,11 @@ +#pragma warning disable CS1591 + using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Providers { /// <summary> - /// Marker interface + /// Marker interface. /// </summary> public interface IMetadataProvider { diff --git a/MediaBrowser.Controller/Providers/IMetadataService.cs b/MediaBrowser.Controller/Providers/IMetadataService.cs index 49f6a7830..05fbb18ee 100644 --- a/MediaBrowser.Controller/Providers/IMetadataService.cs +++ b/MediaBrowser.Controller/Providers/IMetadataService.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Threading; using System.Threading.Tasks; @@ -9,11 +11,18 @@ namespace MediaBrowser.Controller.Providers public interface IMetadataService { /// <summary> + /// Gets the order. + /// </summary> + /// <value>The order.</value> + int Order { get; } + + /// <summary> /// Determines whether this instance can refresh the specified item. /// </summary> /// <param name="item">The item.</param> - /// <returns><c>true</c> if this instance can refresh the specified item; otherwise, <c>false</c>.</returns> + /// <returns><c>true</c> if this instance can refresh the specified item.</returns> bool CanRefresh(BaseItem item); + bool CanRefreshPrimary(Type type); /// <summary> @@ -24,11 +33,5 @@ namespace MediaBrowser.Controller.Providers /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken); - - /// <summary> - /// Gets the order. - /// </summary> - /// <value>The order.</value> - int Order { get; } } } diff --git a/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs b/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs index 058010e1a..6d98af33e 100644 --- a/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs +++ b/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs @@ -1,7 +1,8 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Providers { public interface IPreRefreshProvider : ICustomMetadataProvider { - } } diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs index 925ace895..9f7a76be6 100644 --- a/MediaBrowser.Controller/Providers/IProviderManager.cs +++ b/MediaBrowser.Controller/Providers/IProviderManager.cs @@ -1,26 +1,39 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.IO; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Net; +using Jellyfin.Data.Events; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Providers; namespace MediaBrowser.Controller.Providers { /// <summary> - /// Interface IProviderManager + /// Interface IProviderManager. /// </summary> public interface IProviderManager { + event EventHandler<GenericEventArgs<BaseItem>> RefreshStarted; + + event EventHandler<GenericEventArgs<BaseItem>> RefreshCompleted; + + event EventHandler<GenericEventArgs<Tuple<BaseItem, double>>> RefreshProgress; + /// <summary> /// Queues the refresh. /// </summary> + /// <param name="itemId">Item ID.</param> + /// <param name="options">MetadataRefreshOptions for operation.</param> + /// <param name="priority">RefreshPriority for operation.</param> void QueueRefresh(Guid itemId, MetadataRefreshOptions options, RefreshPriority priority); /// <summary> @@ -42,6 +55,14 @@ namespace MediaBrowser.Controller.Providers Task<ItemUpdateType> RefreshSingleItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken); /// <summary> + /// Runs multiple metadata refreshes concurrently. + /// </summary> + /// <param name="action">The action to run.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns> + Task RunMetadataRefresh(Func<Task> action, CancellationToken cancellationToken); + + /// <summary> /// Saves the image. /// </summary> /// <param name="item">The item.</param> @@ -67,14 +88,31 @@ namespace MediaBrowser.Controller.Providers /// <summary> /// Saves the image. /// </summary> + /// <param name="item">Image to save.</param> + /// <param name="source">Source of image.</param> + /// <param name="mimeType">Mime type image.</param> + /// <param name="type">Type of image.</param> + /// <param name="imageIndex">Index of image.</param> + /// <param name="saveLocallyWithMedia">Option to save locally.</param> + /// <param name="cancellationToken">CancellationToken to use with operation.</param> /// <returns>Task.</returns> Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken); + Task SaveImage(Stream source, string mimeType, string path); + /// <summary> /// Adds the metadata providers. /// </summary> - void AddParts(IEnumerable<IImageProvider> imageProviders, IEnumerable<IMetadataService> metadataServices, IEnumerable<IMetadataProvider> metadataProviders, - IEnumerable<IMetadataSaver> savers, + /// <param name="imageProviders">Image providers to use.</param> + /// <param name="metadataServices">Metadata services to use.</param> + /// <param name="metadataProviders">Metadata providers to use.</param> + /// <param name="metadataSavers">Metadata savers to use.</param> + /// <param name="externalIds">External IDs to use.</param> + void AddParts( + IEnumerable<IImageProvider> imageProviders, + IEnumerable<IMetadataService> metadataServices, + IEnumerable<IMetadataProvider> metadataProviders, + IEnumerable<IMetadataSaver> metadataSavers, IEnumerable<IExternalId> externalIds); /// <summary> @@ -118,12 +156,14 @@ namespace MediaBrowser.Controller.Providers /// </summary> /// <param name="item">The item.</param> /// <param name="updateType">Type of the update.</param> - /// <returns>Task.</returns> void SaveMetadata(BaseItem item, ItemUpdateType updateType); /// <summary> /// Saves the metadata. /// </summary> + /// <param name="item">The item.</param> + /// <param name="updateType">Type of the update.</param> + /// <param name="savers">The metadata savers.</param> void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<string> savers); /// <summary> @@ -154,25 +194,16 @@ namespace MediaBrowser.Controller.Providers /// <param name="url">The URL.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{HttpResponseInfo}.</returns> - Task<HttpResponseInfo> GetSearchImage(string providerName, string url, CancellationToken cancellationToken); + Task<HttpResponseMessage> GetSearchImage(string providerName, string url, CancellationToken cancellationToken); Dictionary<Guid, Guid> GetRefreshQueue(); void OnRefreshStart(BaseItem item); + void OnRefreshProgress(BaseItem item, double progress); + void OnRefreshComplete(BaseItem item); double? GetRefreshProgress(Guid id); - - event EventHandler<GenericEventArgs<BaseItem>> RefreshStarted; - event EventHandler<GenericEventArgs<BaseItem>> RefreshCompleted; - event EventHandler<GenericEventArgs<Tuple<BaseItem, double>>> RefreshProgress; - } - - public enum RefreshPriority - { - High = 0, - Normal = 1, - Low = 2 } } diff --git a/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs b/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs index e56bba3e3..de1631dcf 100644 --- a/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs +++ b/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; @@ -9,7 +9,7 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Controller.Providers { /// <summary> - /// Interface IImageProvider + /// Interface IImageProvider. /// </summary> public interface IRemoteImageProvider : IImageProvider { @@ -34,6 +34,6 @@ namespace MediaBrowser.Controller.Providers /// <param name="url">The URL.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{HttpResponseInfo}.</returns> - Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken); + Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs b/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs index c143b15cd..f146decb6 100644 --- a/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs +++ b/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs b/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs index fdb0c8eb5..e401ed211 100644 --- a/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs +++ b/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs @@ -1,6 +1,8 @@ +#pragma warning disable CS1591 + +using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Net; namespace MediaBrowser.Controller.Providers { @@ -12,6 +14,6 @@ namespace MediaBrowser.Controller.Providers /// <param name="url">The URL.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{HttpResponseInfo}.</returns> - Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken); + Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs index aac41369c..2ac4c728b 100644 --- a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs +++ b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CA1819, CS1591 + using System; using System.Linq; using MediaBrowser.Model.Entities; @@ -6,14 +10,6 @@ namespace MediaBrowser.Controller.Providers { public class ImageRefreshOptions { - public MetadataRefreshMode ImageRefreshMode { get; set; } - public IDirectoryService DirectoryService { get; private set; } - - public bool ReplaceAllImages { get; set; } - - public ImageType[] ReplaceImages { get; set; } - public bool IsAutomated { get; set; } - public ImageRefreshOptions(IDirectoryService directoryService) { ImageRefreshMode = MetadataRefreshMode.Default; @@ -23,6 +19,16 @@ namespace MediaBrowser.Controller.Providers IsAutomated = true; } + public MetadataRefreshMode ImageRefreshMode { get; set; } + + public IDirectoryService DirectoryService { get; private set; } + + public bool ReplaceAllImages { get; set; } + + public ImageType[] ReplaceImages { get; set; } + + public bool IsAutomated { get; set; } + public bool IsReplacingImage(ImageType type) { return ImageRefreshMode == MetadataRefreshMode.FullRefresh && diff --git a/MediaBrowser.Controller/Providers/ItemInfo.cs b/MediaBrowser.Controller/Providers/ItemInfo.cs index f29a8aa70..b8dd416a2 100644 --- a/MediaBrowser.Controller/Providers/ItemInfo.cs +++ b/MediaBrowser.Controller/Providers/ItemInfo.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Entities; @@ -12,8 +16,7 @@ namespace MediaBrowser.Controller.Providers ContainingFolderPath = item.ContainingFolderPath; IsInMixedFolder = item.IsInMixedFolder; - var video = item as Video; - if (video != null) + if (item is Video video) { VideoType = video.VideoType; IsPlaceHolder = video.IsPlaceHolder; @@ -23,10 +26,15 @@ namespace MediaBrowser.Controller.Providers } public Type ItemType { get; set; } + public string Path { get; set; } + public string ContainingFolderPath { get; set; } + public VideoType VideoType { get; set; } + public bool IsInMixedFolder { get; set; } + public bool IsPlaceHolder { get; set; } } } diff --git a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs index 0aaab9a94..460f4e500 100644 --- a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs +++ b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CA2227, CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.Entities; @@ -6,40 +10,60 @@ namespace MediaBrowser.Controller.Providers { public class ItemLookupInfo : IHasProviderIds { + public ItemLookupInfo() + { + IsAutomated = true; + ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + /// <summary> /// Gets or sets the name. /// </summary> /// <value>The name.</value> public string Name { get; set; } + + /// <summary> + /// Gets or sets the original title. + /// </summary> + /// <value>The original title of the item.</value> + public string OriginalTitle { get; set; } + + /// <summary> + /// Gets or sets the path. + /// </summary> + /// <value>The path.</value> + public string Path { get; set; } + /// <summary> /// Gets or sets the metadata language. /// </summary> /// <value>The metadata language.</value> public string MetadataLanguage { get; set; } + /// <summary> /// Gets or sets the metadata country code. /// </summary> /// <value>The metadata country code.</value> public string MetadataCountryCode { get; set; } + /// <summary> /// Gets or sets the provider ids. /// </summary> /// <value>The provider ids.</value> public Dictionary<string, string> ProviderIds { get; set; } + /// <summary> /// Gets or sets the year. /// </summary> /// <value>The year.</value> public int? Year { get; set; } + public int? IndexNumber { get; set; } + public int? ParentIndexNumber { get; set; } + public DateTime? PremiereDate { get; set; } - public bool IsAutomated { get; set; } - public ItemLookupInfo() - { - IsAutomated = true; - ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } + public bool IsAutomated { get; set; } } } diff --git a/MediaBrowser.Controller/Providers/LocalImageInfo.cs b/MediaBrowser.Controller/Providers/LocalImageInfo.cs index 24cded79b..a8e70e6d0 100644 --- a/MediaBrowser.Controller/Providers/LocalImageInfo.cs +++ b/MediaBrowser.Controller/Providers/LocalImageInfo.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; @@ -6,6 +10,7 @@ namespace MediaBrowser.Controller.Providers public class LocalImageInfo { public FileSystemMetadata FileInfo { get; set; } + public ImageType Type { get; set; } } } diff --git a/MediaBrowser.Controller/Providers/MetadataProviderPriority.cs b/MediaBrowser.Controller/Providers/MetadataProviderPriority.cs deleted file mode 100644 index 0076bb972..000000000 --- a/MediaBrowser.Controller/Providers/MetadataProviderPriority.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace MediaBrowser.Controller.Providers -{ - /// <summary> - /// Determines when a provider should execute, relative to others - /// </summary> - public enum MetadataProviderPriority - { - // Run this provider at the beginning - /// <summary> - /// The first - /// </summary> - First = 1, - - // Run this provider after all first priority providers - /// <summary> - /// The second - /// </summary> - Second = 2, - - // Run this provider after all second priority providers - /// <summary> - /// The third - /// </summary> - Third = 3, - - /// <summary> - /// The fourth - /// </summary> - Fourth = 4, - - Fifth = 5, - - // Run this provider last - /// <summary> - /// The last - /// </summary> - Last = 999 - } -} diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshMode.cs b/MediaBrowser.Controller/Providers/MetadataRefreshMode.cs index 02152ee33..920e3da5b 100644 --- a/MediaBrowser.Controller/Providers/MetadataRefreshMode.cs +++ b/MediaBrowser.Controller/Providers/MetadataRefreshMode.cs @@ -1,24 +1,26 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Providers { public enum MetadataRefreshMode { /// <summary> - /// The none + /// The none. /// </summary> None = 0, /// <summary> - /// The validation only + /// The validation only. /// </summary> ValidationOnly = 1, /// <summary> - /// Providers will be executed based on default rules + /// Providers will be executed based on default rules. /// </summary> Default = 2, /// <summary> - /// All providers will be executed to search for new metadata + /// All providers will be executed to search for new metadata. /// </summary> FullRefresh = 3 } diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs index b3eb8cdd1..a42c7f8b5 100644 --- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs +++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CA1819, CS1591 + using System; using System.Linq; using MediaBrowser.Controller.Entities; @@ -7,19 +11,6 @@ namespace MediaBrowser.Controller.Providers { public class MetadataRefreshOptions : ImageRefreshOptions { - /// <summary> - /// When paired with MetadataRefreshMode=FullRefresh, all existing data will be overwritten with new data from the providers. - /// </summary> - public bool ReplaceAllMetadata { get; set; } - - public MetadataRefreshMode MetadataRefreshMode { get; set; } - public RemoteSearchResult SearchResult { get; set; } - - public string[] RefreshPaths { get; set; } - - public bool ForceSave { get; set; } - public bool EnableRemoteContentProbe { get; set; } - public MetadataRefreshOptions(IDirectoryService directoryService) : base(directoryService) { @@ -41,15 +32,28 @@ namespace MediaBrowser.Controller.Providers if (copy.RefreshPaths != null && copy.RefreshPaths.Length > 0) { - if (RefreshPaths == null) - { - RefreshPaths = Array.Empty<string>(); - } + RefreshPaths ??= Array.Empty<string>(); RefreshPaths = copy.RefreshPaths.ToArray(); } } + /// <summary> + /// Gets or sets a value indicating whether all existing data should be overwritten with new data from providers + /// when paired with MetadataRefreshMode=FullRefresh. + /// </summary> + public bool ReplaceAllMetadata { get; set; } + + public MetadataRefreshMode MetadataRefreshMode { get; set; } + + public RemoteSearchResult SearchResult { get; set; } + + public string[] RefreshPaths { get; set; } + + public bool ForceSave { get; set; } + + public bool EnableRemoteContentProbe { get; set; } + public bool RefreshItem(BaseItem item) { if (RefreshPaths != null && RefreshPaths.Length > 0) diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs index f4b915c06..2085ae4ad 100644 --- a/MediaBrowser.Controller/Providers/MetadataResult.cs +++ b/MediaBrowser.Controller/Providers/MetadataResult.cs @@ -1,39 +1,61 @@ +#nullable disable + +#pragma warning disable CA1002, CA2227, CS1591 + using System; using System.Collections.Generic; +using System.Globalization; using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Providers { public class MetadataResult<T> { - public List<LocalImageInfo> Images { get; set; } - public List<UserItemData> UserDataList { get; set; } + // 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; public MetadataResult() { - Images = new List<LocalImageInfo>(); ResultLanguage = "en"; } + public List<LocalImageInfo> Images + { + get => _images ??= new List<LocalImageInfo>(); + set => _images = value; + } + + public List<(string url, ImageType type)> RemoteImages + { + get => _remoteImages ??= new List<(string url, ImageType type)>(); + set => _remoteImages = value; + } + + public List<UserItemData> UserDataList { get; set; } + public List<PersonInfo> People { get; set; } public bool HasMetadata { get; set; } + public T Item { get; set; } + public string ResultLanguage { get; set; } + public string Provider { get; set; } + public bool QueriedById { get; set; } + public void AddPerson(PersonInfo p) { - if (People == null) - { - People = new List<PersonInfo>(); - } + People ??= new List<PersonInfo>(); PeopleHelper.AddPerson(People, p); } /// <summary> - /// Not only does this clear, but initializes the list so that services can differentiate between a null list and zero people + /// Not only does this clear, but initializes the list so that services can differentiate between a null list and zero people. /// </summary> public void ResetPeople() { @@ -41,21 +63,21 @@ namespace MediaBrowser.Controller.Providers { People = new List<PersonInfo>(); } - People.Clear(); + else + { + People.Clear(); + } } public UserItemData GetOrAddUserData(string userId) { - if (UserDataList == null) - { - UserDataList = new List<UserItemData>(); - } + UserDataList ??= new List<UserItemData>(); UserItemData userData = null; foreach (var i in UserDataList) { - if (string.Equals(userId, i.UserId.ToString("N"), StringComparison.OrdinalIgnoreCase)) + if (string.Equals(userId, i.UserId.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase)) { userData = i; } diff --git a/MediaBrowser.Controller/Providers/MovieInfo.cs b/MediaBrowser.Controller/Providers/MovieInfo.cs index c9a766fe7..20e6b697a 100644 --- a/MediaBrowser.Controller/Providers/MovieInfo.cs +++ b/MediaBrowser.Controller/Providers/MovieInfo.cs @@ -1,7 +1,8 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Providers { public class MovieInfo : ItemLookupInfo { - } } diff --git a/MediaBrowser.Controller/Providers/MusicVideoInfo.cs b/MediaBrowser.Controller/Providers/MusicVideoInfo.cs index 194b26484..322320abd 100644 --- a/MediaBrowser.Controller/Providers/MusicVideoInfo.cs +++ b/MediaBrowser.Controller/Providers/MusicVideoInfo.cs @@ -1,7 +1,13 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System.Collections.Generic; + namespace MediaBrowser.Controller.Providers { public class MusicVideoInfo : ItemLookupInfo { - public string[] Artists { get; set; } + public IReadOnlyList<string> Artists { get; set; } } } diff --git a/MediaBrowser.Controller/Providers/PersonLookupInfo.cs b/MediaBrowser.Controller/Providers/PersonLookupInfo.cs index 3fa6e50b7..11cb71f90 100644 --- a/MediaBrowser.Controller/Providers/PersonLookupInfo.cs +++ b/MediaBrowser.Controller/Providers/PersonLookupInfo.cs @@ -1,7 +1,8 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Providers { public class PersonLookupInfo : ItemLookupInfo { - } } diff --git a/MediaBrowser.Controller/Providers/RefreshPriority.cs b/MediaBrowser.Controller/Providers/RefreshPriority.cs new file mode 100644 index 000000000..3619f679d --- /dev/null +++ b/MediaBrowser.Controller/Providers/RefreshPriority.cs @@ -0,0 +1,23 @@ +namespace MediaBrowser.Controller.Providers +{ + /// <summary> + /// Provider refresh priority. + /// </summary> + public enum RefreshPriority + { + /// <summary> + /// High priority. + /// </summary> + High = 0, + + /// <summary> + /// Normal priority. + /// </summary> + Normal = 1, + + /// <summary> + /// Low priority. + /// </summary> + Low = 2 + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs b/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs index 078125673..d4df5fa0d 100644 --- a/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs +++ b/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.Providers @@ -10,14 +14,14 @@ namespace MediaBrowser.Controller.Providers public Guid ItemId { get; set; } /// <summary> - /// If set will only search within the given provider + /// Gets or sets the provider name to search within if set. /// </summary> public string SearchProviderName { get; set; } /// <summary> - /// Gets or sets a value indicating whether [include disabled providers]. + /// Gets or sets a value indicating whether disabled providers should be included. /// </summary> - /// <value><c>true</c> if [include disabled providers]; otherwise, <c>false</c>.</value> + /// <value><c>true</c> if disabled providers should be included.</value> public bool IncludeDisabledProviders { get; set; } } } diff --git a/MediaBrowser.Controller/Providers/SeasonInfo.cs b/MediaBrowser.Controller/Providers/SeasonInfo.cs index dd2ef9ad7..1edceb0e4 100644 --- a/MediaBrowser.Controller/Providers/SeasonInfo.cs +++ b/MediaBrowser.Controller/Providers/SeasonInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA2227, CS1591 + using System; using System.Collections.Generic; @@ -5,11 +7,11 @@ namespace MediaBrowser.Controller.Providers { public class SeasonInfo : ItemLookupInfo { - public Dictionary<string, string> SeriesProviderIds { get; set; } - public SeasonInfo() { SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } + + public Dictionary<string, string> SeriesProviderIds { get; set; } } } diff --git a/MediaBrowser.Controller/Providers/SeriesInfo.cs b/MediaBrowser.Controller/Providers/SeriesInfo.cs index 6c206e031..976fa175a 100644 --- a/MediaBrowser.Controller/Providers/SeriesInfo.cs +++ b/MediaBrowser.Controller/Providers/SeriesInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Providers { public class SeriesInfo : ItemLookupInfo diff --git a/MediaBrowser.Controller/Providers/SongInfo.cs b/MediaBrowser.Controller/Providers/SongInfo.cs index 61e950130..4b64a8a98 100644 --- a/MediaBrowser.Controller/Providers/SongInfo.cs +++ b/MediaBrowser.Controller/Providers/SongInfo.cs @@ -1,17 +1,24 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; +using System.Collections.Generic; namespace MediaBrowser.Controller.Providers { public class SongInfo : ItemLookupInfo { - public string[] AlbumArtists { get; set; } - public string Album { get; set; } - public string[] Artists { get; set; } - public SongInfo() { Artists = Array.Empty<string>(); AlbumArtists = Array.Empty<string>(); } + + public IReadOnlyList<string> AlbumArtists { get; set; } + + public string Album { get; set; } + + public IReadOnlyList<string> Artists { get; set; } } } diff --git a/MediaBrowser.Controller/Providers/TrailerInfo.cs b/MediaBrowser.Controller/Providers/TrailerInfo.cs index 13f07562d..630850f9d 100644 --- a/MediaBrowser.Controller/Providers/TrailerInfo.cs +++ b/MediaBrowser.Controller/Providers/TrailerInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Providers { public class TrailerInfo : ItemLookupInfo diff --git a/MediaBrowser.Controller/Providers/VideoContentType.cs b/MediaBrowser.Controller/Providers/VideoContentType.cs index c3b8964a3..49d587f6c 100644 --- a/MediaBrowser.Controller/Providers/VideoContentType.cs +++ b/MediaBrowser.Controller/Providers/VideoContentType.cs @@ -1,17 +1,17 @@ namespace MediaBrowser.Controller.Providers { /// <summary> - /// Enum VideoContentType + /// Enum VideoContentType. /// </summary> public enum VideoContentType { /// <summary> - /// The episode + /// The episode. /// </summary> Episode = 0, /// <summary> - /// The movie + /// The movie. /// </summary> Movie = 1 } diff --git a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs new file mode 100644 index 000000000..ec3706773 --- /dev/null +++ b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading.Tasks; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.QuickConnect; + +namespace MediaBrowser.Controller.QuickConnect +{ + /// <summary> + /// Quick connect standard interface. + /// </summary> + public interface IQuickConnect + { + /// <summary> + /// Gets a value indicating whether quick connect is enabled or not. + /// </summary> + bool IsEnabled { get; } + + /// <summary> + /// Initiates a new quick connect request. + /// </summary> + /// <param name="authorizationInfo">The initiator authorization info.</param> + /// <returns>A quick connect result with tokens to proceed or throws an exception if not active.</returns> + QuickConnectResult TryConnect(AuthorizationInfo authorizationInfo); + + /// <summary> + /// Checks the status of an individual request. + /// </summary> + /// <param name="secret">Unique secret identifier of the request.</param> + /// <returns>Quick connect result.</returns> + QuickConnectResult CheckRequestStatus(string secret); + + /// <summary> + /// Authorizes a quick connect request to connect as the calling user. + /// </summary> + /// <param name="userId">User id.</param> + /// <param name="code">Identifying code for the request.</param> + /// <returns>A boolean indicating if the authorization completed successfully.</returns> + Task<bool> AuthorizeRequest(Guid userId, string code); + + /// <summary> + /// Gets the authorized request for the secret. + /// </summary> + /// <param name="secret">The secret.</param> + /// <returns>The authentication result.</returns> + AuthenticationResult GetAuthorizedRequest(string secret); + } +} diff --git a/MediaBrowser.Controller/Resolvers/IItemResolver.cs b/MediaBrowser.Controller/Resolvers/IItemResolver.cs index 16e37d249..b95d00aa3 100644 --- a/MediaBrowser.Controller/Resolvers/IItemResolver.cs +++ b/MediaBrowser.Controller/Resolvers/IItemResolver.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -7,26 +9,28 @@ using MediaBrowser.Model.IO; namespace MediaBrowser.Controller.Resolvers { /// <summary> - /// Interface IItemResolver + /// Interface IItemResolver. /// </summary> public interface IItemResolver { /// <summary> + /// Gets the priority. + /// </summary> + /// <value>The priority.</value> + ResolverPriority Priority { get; } + + /// <summary> /// Resolves the path. /// </summary> /// <param name="args">The args.</param> /// <returns>BaseItem.</returns> BaseItem ResolvePath(ItemResolveArgs args); - /// <summary> - /// Gets the priority. - /// </summary> - /// <value>The priority.</value> - ResolverPriority Priority { get; } } public interface IMultiItemResolver { - MultiItemResolverResult ResolveMultiple(Folder parent, + MultiItemResolverResult ResolveMultiple( + Folder parent, List<FileSystemMetadata> files, string collectionType, IDirectoryService directoryService); @@ -34,13 +38,14 @@ namespace MediaBrowser.Controller.Resolvers public class MultiItemResolverResult { - public List<BaseItem> Items { get; set; } - public List<FileSystemMetadata> ExtraFiles { get; set; } - public MultiItemResolverResult() { Items = new List<BaseItem>(); ExtraFiles = new List<FileSystemMetadata>(); } + + public List<BaseItem> Items { get; set; } + + public List<FileSystemMetadata> ExtraFiles { get; set; } } } diff --git a/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs b/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs index b40cc157a..a07b3e898 100644 --- a/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs +++ b/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs @@ -4,10 +4,16 @@ using MediaBrowser.Model.IO; namespace MediaBrowser.Controller.Resolvers { /// <summary> - /// Provides a base "rule" that anyone can use to have paths ignored by the resolver + /// Provides a base "rule" that anyone can use to have paths ignored by the resolver. /// </summary> public interface IResolverIgnoreRule { + /// <summary> + /// Checks whether or not the file should be ignored. + /// </summary> + /// <param name="fileInfo">The file information.</param> + /// <param name="parent">The parent BaseItem.</param> + /// <returns>True if the file should be ignored.</returns> bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent); } } diff --git a/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs b/MediaBrowser.Controller/Resolvers/ItemResolver.cs index 637a7e3f0..7fd54fcc6 100644 --- a/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs +++ b/MediaBrowser.Controller/Resolvers/ItemResolver.cs @@ -1,33 +1,35 @@ +#nullable disable + using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; namespace MediaBrowser.Controller.Resolvers { /// <summary> - /// Class ItemResolver + /// Class ItemResolver. /// </summary> - /// <typeparam name="T"></typeparam> + /// <typeparam name="T">The type of BaseItem.</typeparam> public abstract class ItemResolver<T> : IItemResolver where T : BaseItem, new() { /// <summary> + /// Gets the priority. + /// </summary> + /// <value>The priority.</value> + public virtual ResolverPriority Priority => ResolverPriority.First; + + /// <summary> /// Resolves the specified args. /// </summary> /// <param name="args">The args.</param> /// <returns>`0.</returns> - protected virtual T Resolve(ItemResolveArgs args) + public virtual T Resolve(ItemResolveArgs args) { return null; } /// <summary> - /// Gets the priority. - /// </summary> - /// <value>The priority.</value> - public virtual ResolverPriority Priority => ResolverPriority.First; - - /// <summary> - /// Sets initial values on the newly resolved item + /// Sets initial values on the newly resolved item. /// </summary> /// <param name="item">The item.</param> /// <param name="args">The args.</param> diff --git a/MediaBrowser.Controller/Resolvers/ResolverPriority.cs b/MediaBrowser.Controller/Resolvers/ResolverPriority.cs index e39310095..d4f975b6d 100644 --- a/MediaBrowser.Controller/Resolvers/ResolverPriority.cs +++ b/MediaBrowser.Controller/Resolvers/ResolverPriority.cs @@ -1,26 +1,38 @@ namespace MediaBrowser.Controller.Resolvers { /// <summary> - /// Enum ResolverPriority + /// Enum ResolverPriority. /// </summary> public enum ResolverPriority { /// <summary> - /// The first + /// The first. /// </summary> First = 1, + /// <summary> - /// The second + /// The second. /// </summary> Second = 2, + /// <summary> - /// The third + /// The third. /// </summary> Third = 3, + + /// <summary> + /// The Fourth. + /// </summary> Fourth = 4, + + /// <summary> + /// The Fifth. + /// </summary> + Fifth = 5, + /// <summary> - /// The last + /// The last. /// </summary> - Last = 5 + Last = 6 } } diff --git a/MediaBrowser.Controller/Security/AuthenticationInfo.cs b/MediaBrowser.Controller/Security/AuthenticationInfo.cs index 828213588..b4b242f1b 100644 --- a/MediaBrowser.Controller/Security/AuthenticationInfo.cs +++ b/MediaBrowser.Controller/Security/AuthenticationInfo.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.Security @@ -65,6 +69,7 @@ namespace MediaBrowser.Controller.Security public DateTime? DateRevoked { get; set; } public DateTime DateLastActivity { get; set; } + public string UserName { get; set; } } } diff --git a/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs b/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs deleted file mode 100644 index 2bd17eb26..000000000 --- a/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; - -namespace MediaBrowser.Controller.Security -{ - public class AuthenticationInfoQuery - { - /// <summary> - /// Gets or sets the device identifier. - /// </summary> - /// <value>The device identifier.</value> - public string DeviceId { get; set; } - - /// <summary> - /// Gets or sets the user identifier. - /// </summary> - /// <value>The user identifier.</value> - public Guid UserId { get; set; } - - /// <summary> - /// Gets or sets the access token. - /// </summary> - /// <value>The access token.</value> - public string AccessToken { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is active. - /// </summary> - /// <value><c>null</c> if [is active] contains no value, <c>true</c> if [is active]; otherwise, <c>false</c>.</value> - public bool? IsActive { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance has user. - /// </summary> - /// <value><c>null</c> if [has user] contains no value, <c>true</c> if [has user]; otherwise, <c>false</c>.</value> - public bool? HasUser { get; set; } - - /// <summary> - /// Gets or sets the start index. - /// </summary> - /// <value>The start index.</value> - public int? StartIndex { get; set; } - - /// <summary> - /// Gets or sets the limit. - /// </summary> - /// <value>The limit.</value> - public int? Limit { get; set; } - } -} diff --git a/MediaBrowser.Controller/Security/IAuthenticationManager.cs b/MediaBrowser.Controller/Security/IAuthenticationManager.cs new file mode 100644 index 000000000..e3d18c8c0 --- /dev/null +++ b/MediaBrowser.Controller/Security/IAuthenticationManager.cs @@ -0,0 +1,33 @@ +#nullable enable + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Security +{ + /// <summary> + /// Handles the retrieval and storage of API keys. + /// </summary> + public interface IAuthenticationManager + { + /// <summary> + /// Creates an API key. + /// </summary> + /// <param name="name">The name of the key.</param> + /// <returns>A task representing the creation of the key.</returns> + Task CreateApiKey(string name); + + /// <summary> + /// Gets the API keys. + /// </summary> + /// <returns>A task representing the retrieval of the API keys.</returns> + Task<IReadOnlyList<AuthenticationInfo>> GetApiKeys(); + + /// <summary> + /// Deletes an API key with the provided access token. + /// </summary> + /// <param name="accessToken">The access token.</param> + /// <returns>A task representing the deletion of the API key.</returns> + Task DeleteApiKey(string accessToken); + } +} diff --git a/MediaBrowser.Controller/Security/IAuthenticationRepository.cs b/MediaBrowser.Controller/Security/IAuthenticationRepository.cs deleted file mode 100644 index 6a9625613..000000000 --- a/MediaBrowser.Controller/Security/IAuthenticationRepository.cs +++ /dev/null @@ -1,34 +0,0 @@ -using MediaBrowser.Model.Devices; -using MediaBrowser.Model.Querying; - -namespace MediaBrowser.Controller.Security -{ - public interface IAuthenticationRepository - { - /// <summary> - /// Creates the specified information. - /// </summary> - /// <param name="info">The information.</param> - /// <returns>Task.</returns> - void Create(AuthenticationInfo info); - - /// <summary> - /// Updates the specified information. - /// </summary> - /// <param name="info">The information.</param> - /// <returns>Task.</returns> - void Update(AuthenticationInfo info); - - /// <summary> - /// Gets the specified query. - /// </summary> - /// <param name="query">The query.</param> - /// <returns>QueryResult{AuthenticationInfo}.</returns> - QueryResult<AuthenticationInfo> Get(AuthenticationInfoQuery query); - - void Delete(AuthenticationInfo info); - - DeviceOptions GetDeviceOptions(string deviceId); - void UpdateDeviceOptions(string deviceId, DeviceOptions options); - } -} diff --git a/MediaBrowser.Controller/Security/IEncryptionManager.cs b/MediaBrowser.Controller/Security/IEncryptionManager.cs deleted file mode 100644 index 68680fdf3..000000000 --- a/MediaBrowser.Controller/Security/IEncryptionManager.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace MediaBrowser.Controller.Security -{ - public interface IEncryptionManager - { - /// <summary> - /// Encrypts the string. - /// </summary> - /// <param name="value">The value.</param> - /// <returns>System.String.</returns> - string EncryptString(string value); - - /// <summary> - /// Decrypts the string. - /// </summary> - /// <param name="value">The value.</param> - /// <returns>System.String.</returns> - string DecryptString(string value); - } -} diff --git a/MediaBrowser.Controller/Session/AuthenticationRequest.cs b/MediaBrowser.Controller/Session/AuthenticationRequest.cs index a28f47a9c..647c75e66 100644 --- a/MediaBrowser.Controller/Session/AuthenticationRequest.cs +++ b/MediaBrowser.Controller/Session/AuthenticationRequest.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.Session @@ -5,13 +9,22 @@ namespace MediaBrowser.Controller.Session public class AuthenticationRequest { public string Username { get; set; } + public Guid UserId { get; set; } + public string Password { get; set; } + + [Obsolete("Send full password in Password field")] public string PasswordSha1 { get; set; } + public string App { get; set; } + public string AppVersion { get; set; } + public string DeviceId { get; set; } + public string DeviceName { get; set; } + public string RemoteEndPoint { get; set; } } } diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs index a59c96ac7..b38ee1146 100644 --- a/MediaBrowser.Controller/Session/ISessionController.cs +++ b/MediaBrowser.Controller/Session/ISessionController.cs @@ -1,5 +1,11 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Model.Session; namespace MediaBrowser.Controller.Session { @@ -20,6 +26,12 @@ namespace MediaBrowser.Controller.Session /// <summary> /// Sends the message. /// </summary> - Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken); + /// <typeparam name="T">The type of data.</typeparam> + /// <param name="name">Name of message type.</param> + /// <param name="messageId">Message ID.</param> + /// <param name="data">Data to send.</param> + /// <param name="cancellationToken">CancellationToken for operation.</param> + /// <returns>A task.</returns> + Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 771027103..c86556095 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -1,19 +1,22 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities.Security; +using Jellyfin.Data.Events; using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Security; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Session; +using MediaBrowser.Model.SyncPlay; namespace MediaBrowser.Controller.Session { /// <summary> - /// Interface ISessionManager + /// Interface ISessionManager. /// </summary> public interface ISessionManager { @@ -45,6 +48,11 @@ namespace MediaBrowser.Controller.Session event EventHandler<SessionEventArgs> SessionActivity; /// <summary> + /// Occurs when [session controller connected]. + /// </summary> + event EventHandler<SessionEventArgs> SessionControllerConnected; + + /// <summary> /// Occurs when [capabilities changed]. /// </summary> event EventHandler<SessionEventArgs> CapabilitiesChanged; @@ -74,40 +82,46 @@ namespace MediaBrowser.Controller.Session /// <param name="deviceName">Name of the device.</param> /// <param name="remoteEndPoint">The remote end point.</param> /// <param name="user">The user.</param> - SessionInfo LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user); + /// <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); + + /// <summary> + /// Used to report that a session controller has connected. + /// </summary> + /// <param name="session">The session.</param> + void OnSessionControllerConnected(SessionInfo session); void UpdateDeviceName(string sessionId, string reportedDeviceName); /// <summary> - /// Used to report that playback has started for an item + /// Used to report that playback has started for an item. /// </summary> /// <param name="info">The info.</param> /// <returns>Task.</returns> Task OnPlaybackStart(PlaybackStartInfo info); /// <summary> - /// Used to report playback progress for an item + /// Used to report playback progress for an item. /// </summary> /// <param name="info">The info.</param> /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentNullException">Throws if an argument is null.</exception> Task OnPlaybackProgress(PlaybackProgressInfo info); Task OnPlaybackProgress(PlaybackProgressInfo info, bool isAutomated); /// <summary> - /// Used to report that playback has ended for an item + /// Used to report that playback has ended for an item. /// </summary> /// <param name="info">The info.</param> /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentNullException">Throws if an argument is null.</exception> Task OnPlaybackStopped(PlaybackStopInfo info); /// <summary> /// Reports the session ended. /// </summary> /// <param name="sessionId">The session identifier.</param> - /// <returns>Task.</returns> void ReportSessionEnded(string sessionId); /// <summary> @@ -141,6 +155,25 @@ namespace MediaBrowser.Controller.Session Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken); /// <summary> + /// Sends a SyncPlayCommand to a session. + /// </summary> + /// <param name="sessionId">The identifier of the session.</param> + /// <param name="command">The command.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken); + + /// <summary> + /// Sends a SyncPlayGroupUpdate to a session. + /// </summary> + /// <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> + /// <returns>Task.</returns> + Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancellationToken); + + /// <summary> /// Sends the browse command. /// </summary> /// <param name="controllingSessionId">The controlling session identifier.</param> @@ -163,32 +196,45 @@ namespace MediaBrowser.Controller.Session /// <summary> /// Sends the message to admin sessions. /// </summary> - /// <typeparam name="T"></typeparam> - /// <param name="name">The name.</param> + /// <typeparam name="T">Type of data.</typeparam> + /// <param name="name">Message type name.</param> /// <param name="data">The data.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - Task SendMessageToAdminSessions<T>(string name, T data, CancellationToken cancellationToken); + Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken); /// <summary> /// Sends the message to user sessions. /// </summary> - /// <typeparam name="T"></typeparam> + /// <typeparam name="T">Type of data.</typeparam> + /// <param name="userIds">Users to send messages to.</param> + /// <param name="name">Message type name.</param> + /// <param name="data">The data.</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, T data, CancellationToken cancellationToken); + Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken cancellationToken); - Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, Func<T> dataFn, CancellationToken cancellationToken); + /// <summary> + /// Sends the message to user sessions. + /// </summary> + /// <typeparam name="T">Type of data.</typeparam> + /// <param name="userIds">Users to send messages to.</param> + /// <param name="name">Message type name.</param> + /// <param name="dataFn">Data function.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, CancellationToken cancellationToken); /// <summary> /// Sends the message to user device sessions. /// </summary> - /// <typeparam name="T"></typeparam> + /// <typeparam name="T">Type of data.</typeparam> /// <param name="deviceId">The device identifier.</param> - /// <param name="name">The name.</param> + /// <param name="name">Message type name.</param> /// <param name="data">The data.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - Task SendMessageToUserDeviceSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken); + Task SendMessageToUserDeviceSessions<T>(string deviceId, SessionMessageType name, T data, CancellationToken cancellationToken); /// <summary> /// Sends the restart required message. @@ -233,25 +279,13 @@ namespace MediaBrowser.Controller.Session void ReportNowViewingItem(string sessionId, string itemId); /// <summary> - /// Reports the now viewing item. - /// </summary> - /// <param name="sessionId">The session identifier.</param> - /// <param name="item">The item.</param> - void ReportNowViewingItem(string sessionId, BaseItemDto item); - - /// <summary> /// Authenticates the new session. /// </summary> /// <param name="request">The request.</param> /// <returns>Task{SessionInfo}.</returns> Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request); - /// <summary> - /// Creates the new session. - /// </summary> - /// <param name="request">The request.</param> - /// <returns>Task<AuthenticationResult>.</returns> - Task<AuthenticationResult> CreateNewSession(AuthenticationRequest request); + Task<AuthenticationResult> AuthenticateDirect(AuthenticationRequest request); /// <summary> /// Reports the capabilities. @@ -289,7 +323,7 @@ namespace MediaBrowser.Controller.Session /// <param name="deviceId">The device identifier.</param> /// <param name="remoteEndpoint">The remote endpoint.</param> /// <returns>SessionInfo.</returns> - SessionInfo GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint); + Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint); /// <summary> /// Gets the session by authentication token. @@ -299,28 +333,24 @@ namespace MediaBrowser.Controller.Session /// <param name="remoteEndpoint">The remote endpoint.</param> /// <param name="appVersion">The application version.</param> /// <returns>Task<SessionInfo>.</returns> - SessionInfo GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion); + Task<SessionInfo> GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, string appVersion); /// <summary> /// Logouts the specified access token. /// </summary> /// <param name="accessToken">The access token.</param> - /// <returns>Task.</returns> - void Logout(string accessToken); - void Logout(AuthenticationInfo accessToken); + /// <returns>A <see cref="Task"/> representing the log out process.</returns> + Task Logout(string accessToken); - /// <summary> - /// Revokes the user tokens. - /// </summary> - /// <returns>Task.</returns> - void RevokeUserTokens(Guid userId, string currentAccessToken); + Task Logout(Device device); /// <summary> - /// Revokes the token. + /// Revokes the user tokens. /// </summary> - /// <param name="id">The identifier.</param> + /// <param name="userId">The user's id.</param> + /// <param name="currentAccessToken">The current access token.</param> /// <returns>Task.</returns> - void RevokeToken(string id); + Task RevokeUserTokens(Guid userId, string currentAccessToken); void CloseIfNeeded(SessionInfo session); } diff --git a/MediaBrowser.Controller/Session/SessionEventArgs.cs b/MediaBrowser.Controller/Session/SessionEventArgs.cs index 08baaf647..269fe7dc4 100644 --- a/MediaBrowser.Controller/Session/SessionEventArgs.cs +++ b/MediaBrowser.Controller/Session/SessionEventArgs.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.Session diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index f0e81e8e7..6134c0cf3 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -1,22 +1,36 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; +using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Session { /// <summary> - /// Class SessionInfo + /// Class SessionInfo. /// </summary> - public class SessionInfo : IDisposable + public sealed class SessionInfo : IDisposable { - private ISessionManager _sessionManager; + // 1 second + private const long ProgressIncrement = 10000000; + + private readonly ISessionManager _sessionManager; private readonly ILogger _logger; + private readonly object _progressLock = new object(); + private Timer _progressTimer; + private PlaybackProgressInfo _lastProgressInfo; + + private bool _disposed = false; + public SessionInfo(ISessionManager sessionManager, ILogger logger) { _sessionManager = sessionManager; @@ -40,10 +54,10 @@ namespace MediaBrowser.Controller.Session public string RemoteEndPoint { get; set; } /// <summary> - /// Gets or sets the playable media types. + /// Gets the playable media types. /// </summary> /// <value>The playable media types.</value> - public string[] PlayableMediaTypes + public IReadOnlyList<string> PlayableMediaTypes { get { @@ -51,6 +65,7 @@ namespace MediaBrowser.Controller.Session { return Array.Empty<string>(); } + return Capabilities.PlayableMediaTypes; } } @@ -97,6 +112,10 @@ namespace MediaBrowser.Controller.Session /// <value>The name of the device.</value> public string DeviceName { get; set; } + /// <summary> + /// Gets or sets the type of the device. + /// </summary> + /// <value>The type of the device.</value> public string DeviceType { get; set; } /// <summary> @@ -107,6 +126,8 @@ namespace MediaBrowser.Controller.Session public BaseItem FullNowPlayingItem { get; set; } + public BaseItemDto NowViewingItem { get; set; } + /// <summary> /// Gets or sets the device id. /// </summary> @@ -123,31 +144,9 @@ namespace MediaBrowser.Controller.Session /// Gets or sets the session controller. /// </summary> /// <value>The session controller.</value> - [IgnoreDataMember] + [JsonIgnore] public ISessionController[] SessionControllers { get; set; } - /// <summary> - /// Gets or sets the application icon URL. - /// </summary> - /// <value>The application icon URL.</value> - public string AppIconUrl { get; set; } - - /// <summary> - /// Gets or sets the supported commands. - /// </summary> - /// <value>The supported commands.</value> - public string[] SupportedCommands - { - get - { - if (Capabilities == null) - { - return new string[] { }; - } - return Capabilities.SupportedCommands; - } - } - public TranscodingInfo TranscodingInfo { get; set; } /// <summary> @@ -166,6 +165,7 @@ namespace MediaBrowser.Controller.Session return true; } } + if (controllers.Length > 0) { return false; @@ -219,6 +219,23 @@ namespace MediaBrowser.Controller.Session } } + public QueueItem[] NowPlayingQueue { get; set; } + + public bool HasCustomDeviceName { get; set; } + + public string PlaylistItemId { get; set; } + + public string ServerId { get; set; } + + public string UserPrimaryImageTag { get; set; } + + /// <summary> + /// Gets the supported commands. + /// </summary> + /// <value>The supported commands.</value> + public IReadOnlyList<GeneralCommandType> SupportedCommands + => Capabilities == null ? Array.Empty<GeneralCommandType>() : Capabilities.SupportedCommands; + public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory) { var controllers = SessionControllers.ToList(); @@ -245,11 +262,6 @@ namespace MediaBrowser.Controller.Session SessionControllers = controllers.ToArray(); } - public bool ContainsUser(string userId) - { - return ContainsUser(new Guid(userId)); - } - public bool ContainsUser(Guid userId) { if (UserId.Equals(userId)) @@ -259,18 +271,15 @@ namespace MediaBrowser.Controller.Session foreach (var additionalUser in AdditionalUsers) { - if (userId.Equals(userId)) + if (additionalUser.UserId.Equals(userId)) { return true; } } + return false; } - private readonly object _progressLock = new object(); - private Timer _progressTimer; - private PlaybackProgressInfo _lastProgressInfo; - public void StartAutomaticProgress(PlaybackProgressInfo progressInfo) { if (_disposed) @@ -293,9 +302,6 @@ namespace MediaBrowser.Controller.Session } } - // 1 second - private const long ProgressIncrement = 10000000; - private async void OnProgressTimerCallback(object state) { if (_disposed) @@ -308,6 +314,7 @@ namespace MediaBrowser.Controller.Session { return; } + if (progressInfo.IsPaused) { return; @@ -321,7 +328,7 @@ namespace MediaBrowser.Controller.Session var newPositionTicks = positionTicks + ProgressIncrement; var item = progressInfo.Item; - long? runtimeTicks = item == null ? null : item.RunTimeTicks; + long? runtimeTicks = item?.RunTimeTicks; // Don't report beyond the runtime if (runtimeTicks.HasValue && newPositionTicks >= runtimeTicks.Value) @@ -350,12 +357,12 @@ namespace MediaBrowser.Controller.Session _progressTimer.Dispose(); _progressTimer = null; } + _lastProgressInfo = null; } } - private bool _disposed = false; - + /// <inheritdoc /> public void Dispose() { _disposed = true; @@ -367,30 +374,12 @@ namespace MediaBrowser.Controller.Session foreach (var controller in controllers) { - var disposable = controller as IDisposable; - - if (disposable != null) + if (controller is IDisposable disposable) { _logger.LogDebug("Disposing session controller {0}", disposable.GetType().Name); - - try - { - disposable.Dispose(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error disposing session controller"); - } + disposable.Dispose(); } } - - _sessionManager = null; } - - public QueueItem[] NowPlayingQueue { get; set; } - public bool HasCustomDeviceName { get; set; } - public string PlaylistItemId { get; set; } - public string ServerId { get; set; } - public string UserPrimaryImageTag { get; set; } } } diff --git a/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs index 31087edec..07fe1ea8a 100644 --- a/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs +++ b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs @@ -4,9 +4,9 @@ using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Sorting { /// <summary> - /// Interface IBaseItemComparer + /// Interface IBaseItemComparer. /// </summary> - public interface IBaseItemComparer : IComparer<BaseItem> + public interface IBaseItemComparer : IComparer<BaseItem?> { /// <summary> /// Gets the name. diff --git a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs index 1e2df37bf..bd47db39a 100644 --- a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs +++ b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs @@ -1,10 +1,11 @@ -using MediaBrowser.Controller.Entities; +#nullable disable + 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 it's comparison. /// </summary> public interface IUserBaseItemComparer : IBaseItemComparer { @@ -12,7 +13,7 @@ namespace MediaBrowser.Controller.Sorting /// Gets or sets the user. /// </summary> /// <value>The user.</value> - User User { get; set; } + Jellyfin.Data.Entities.User User { get; set; } /// <summary> /// Gets or sets the user manager. diff --git a/MediaBrowser.Controller/Sorting/SortExtensions.cs b/MediaBrowser.Controller/Sorting/SortExtensions.cs index 111f4f17f..f9c0d39dd 100644 --- a/MediaBrowser.Controller/Sorting/SortExtensions.cs +++ b/MediaBrowser.Controller/Sorting/SortExtensions.cs @@ -1,143 +1,34 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; -using System.Text; +using Jellyfin.Extensions; namespace MediaBrowser.Controller.Sorting { public static class SortExtensions { + private static readonly AlphanumericComparator _comparer = new AlphanumericComparator(); + public static IEnumerable<T> OrderByString<T>(this IEnumerable<T> list, Func<T, string> getName) { - return list.OrderBy(getName, new AlphanumComparator()); + return list.OrderBy(getName, _comparer); } public static IEnumerable<T> OrderByStringDescending<T>(this IEnumerable<T> list, Func<T, string> getName) { - return list.OrderByDescending(getName, new AlphanumComparator()); + return list.OrderByDescending(getName, _comparer); } public static IOrderedEnumerable<T> ThenByString<T>(this IOrderedEnumerable<T> list, Func<T, string> getName) { - return list.ThenBy(getName, new AlphanumComparator()); + return list.ThenBy(getName, _comparer); } public static IOrderedEnumerable<T> ThenByStringDescending<T>(this IOrderedEnumerable<T> list, Func<T, string> getName) { - return list.ThenByDescending(getName, new AlphanumComparator()); - } - - private class AlphanumComparator : IComparer<string> - { - private enum ChunkType { Alphanumeric, Numeric }; - - private static bool InChunk(char ch, char otherCh) - { - var type = ChunkType.Alphanumeric; - - if (char.IsDigit(otherCh)) - { - type = ChunkType.Numeric; - } - - if ((type == ChunkType.Alphanumeric && char.IsDigit(ch)) - || (type == ChunkType.Numeric && !char.IsDigit(ch))) - { - return false; - } - - return true; - } - - public static int CompareValues(string s1, string s2) - { - if (s1 == null || s2 == null) - { - return 0; - } - - int thisMarker = 0, thisNumericChunk = 0; - int thatMarker = 0, thatNumericChunk = 0; - - while ((thisMarker < s1.Length) || (thatMarker < s2.Length)) - { - if (thisMarker >= s1.Length) - { - return -1; - } - else if (thatMarker >= s2.Length) - { - return 1; - } - char thisCh = s1[thisMarker]; - char thatCh = s2[thatMarker]; - - var thisChunk = new StringBuilder(); - var thatChunk = new StringBuilder(); - - while ((thisMarker < s1.Length) && (thisChunk.Length == 0 || InChunk(thisCh, thisChunk[0]))) - { - thisChunk.Append(thisCh); - thisMarker++; - - if (thisMarker < s1.Length) - { - thisCh = s1[thisMarker]; - } - } - - while ((thatMarker < s2.Length) && (thatChunk.Length == 0 || InChunk(thatCh, thatChunk[0]))) - { - thatChunk.Append(thatCh); - thatMarker++; - - if (thatMarker < s2.Length) - { - thatCh = s2[thatMarker]; - } - } - - int result = 0; - // If both chunks contain numeric characters, sort them numerically - if (char.IsDigit(thisChunk[0]) && char.IsDigit(thatChunk[0])) - { - if (!int.TryParse(thisChunk.ToString(), out thisNumericChunk)) - { - return 0; - } - if (!int.TryParse(thatChunk.ToString(), out thatNumericChunk)) - { - return 0; - } - - if (thisNumericChunk < thatNumericChunk) - { - result = -1; - } - - if (thisNumericChunk > thatNumericChunk) - { - result = 1; - } - } - else - { - result = thisChunk.ToString().CompareTo(thatChunk.ToString()); - } - - if (result != 0) - { - return result; - } - } - - return 0; - } - - public int Compare(string x, string y) - { - return CompareValues(x, y); - } + return list.ThenByDescending(getName, _comparer); } } } diff --git a/MediaBrowser.Controller/Sorting/SortHelper.cs b/MediaBrowser.Controller/Sorting/SortHelper.cs deleted file mode 100644 index 05981d975..000000000 --- a/MediaBrowser.Controller/Sorting/SortHelper.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace MediaBrowser.Controller.Sorting -{ - public static class SortHelper - { - private enum ChunkType { Alphanumeric, Numeric }; - - public static bool InChunk(char ch, char otherCh) - { - var type = ChunkType.Alphanumeric; - - if (char.IsDigit(otherCh)) - { - type = ChunkType.Numeric; - } - - if ((type == ChunkType.Alphanumeric && char.IsDigit(ch)) - || (type == ChunkType.Numeric && !char.IsDigit(ch))) - { - return false; - } - - return true; - } - } -} diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs index 0872335c5..52aa44024 100644 --- a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs +++ b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading; @@ -24,9 +28,17 @@ namespace MediaBrowser.Controller.Subtitles /// <summary> /// Searches the subtitles. /// </summary> - Task<RemoteSubtitleInfo[]> SearchSubtitles(Video video, + /// <param name="video">The video.</param> + /// <param name="language">Subtitle language.</param> + /// <param name="isPerfectMatch">Require perfect match.</param> + /// <param name="isAutomated">Request is automated.</param> + /// <param name="cancellationToken">CancellationToken to use for the operation.</param> + /// <returns>Subtitles, wrapped in task.</returns> + Task<RemoteSubtitleInfo[]> SearchSubtitles( + Video video, string language, bool? isPerfectMatch, + bool isAutomated, CancellationToken cancellationToken); /// <summary> @@ -34,36 +46,59 @@ namespace MediaBrowser.Controller.Subtitles /// </summary> /// <param name="request">The request.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{IEnumerable{RemoteSubtitleInfo}}.</returns> - Task<RemoteSubtitleInfo[]> SearchSubtitles(SubtitleSearchRequest request, + /// <returns>Task{RemoteSubtitleInfo[]}.</returns> + Task<RemoteSubtitleInfo[]> SearchSubtitles( + SubtitleSearchRequest request, CancellationToken cancellationToken); /// <summary> /// Downloads the subtitles. /// </summary> + /// <param name="video">The video.</param> + /// <param name="subtitleId">Subtitle ID.</param> + /// <param name="cancellationToken">CancellationToken to use for the operation.</param> + /// <returns>A task.</returns> Task DownloadSubtitles(Video video, string subtitleId, CancellationToken cancellationToken); /// <summary> /// Downloads the subtitles. /// </summary> + /// <param name="video">The video.</param> + /// <param name="libraryOptions">Library options to use.</param> + /// <param name="subtitleId">Subtitle ID.</param> + /// <param name="cancellationToken">CancellationToken to use for the operation.</param> + /// <returns>A task.</returns> Task DownloadSubtitles(Video video, LibraryOptions libraryOptions, string subtitleId, CancellationToken cancellationToken); /// <summary> + /// Upload new subtitle. + /// </summary> + /// <param name="video">The video the subtitle belongs to.</param> + /// <param name="response">The subtitle response.</param> + /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> + Task UploadSubtitle(Video video, SubtitleResponse response); + + /// <summary> /// Gets the remote subtitles. /// </summary> /// <param name="id">The identifier.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{SubtitleResponse}.</returns> + /// <returns><see cref="Task{SubtitleResponse}" />.</returns> Task<SubtitleResponse> GetRemoteSubtitles(string id, CancellationToken cancellationToken); /// <summary> /// Deletes the subtitles. /// </summary> + /// <param name="item">Media item.</param> + /// <param name="index">Subtitle index.</param> + /// <returns>A task.</returns> Task DeleteSubtitles(BaseItem item, int index); /// <summary> /// Gets the providers. /// </summary> + /// <param name="item">The media item.</param> + /// <returns>Subtitles providers.</returns> SubtitleProviderInfo[] GetSupportedProviders(BaseItem item); } } diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs b/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs index 8ffd7c530..326348d58 100644 --- a/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs +++ b/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.Controller/Subtitles/SubtitleDownloadEventArgs.cs b/MediaBrowser.Controller/Subtitles/SubtitleDownloadEventArgs.cs deleted file mode 100644 index 5703aea17..000000000 --- a/MediaBrowser.Controller/Subtitles/SubtitleDownloadEventArgs.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using MediaBrowser.Controller.Entities; - -namespace MediaBrowser.Controller.Subtitles -{ - public class SubtitleDownloadEventArgs - { - public BaseItem Item { get; set; } - - public string Format { get; set; } - - public string Language { get; set; } - - public bool IsForced { get; set; } - - public string Provider { get; set; } - } - - public class SubtitleDownloadFailureEventArgs - { - public BaseItem Item { get; set; } - - public string Provider { get; set; } - - public Exception Exception { get; set; } - } -} diff --git a/MediaBrowser.Controller/Subtitles/SubtitleDownloadFailureEventArgs.cs b/MediaBrowser.Controller/Subtitles/SubtitleDownloadFailureEventArgs.cs new file mode 100644 index 000000000..c782f5796 --- /dev/null +++ b/MediaBrowser.Controller/Subtitles/SubtitleDownloadFailureEventArgs.cs @@ -0,0 +1,28 @@ +#nullable disable + +using System; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Subtitles +{ + /// <summary> + /// An event that occurs when subtitle downloading fails. + /// </summary> + public class SubtitleDownloadFailureEventArgs : EventArgs + { + /// <summary> + /// Gets or sets the item. + /// </summary> + public BaseItem Item { get; set; } + + /// <summary> + /// Gets or sets the provider. + /// </summary> + public string Provider { get; set; } + + /// <summary> + /// Gets or sets the exception. + /// </summary> + public Exception Exception { get; set; } + } +} diff --git a/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs b/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs index b8ba35a5f..85b3e6fbd 100644 --- a/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs +++ b/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System.IO; namespace MediaBrowser.Controller.Subtitles @@ -5,8 +9,11 @@ namespace MediaBrowser.Controller.Subtitles public class SubtitleResponse { public string Language { get; set; } + public string Format { get; set; } + public bool IsForced { get; set; } + public Stream Stream { get; set; } } } diff --git a/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs b/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs index 61dc72258..ef052237a 100644 --- a/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs +++ b/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Controller.Providers; @@ -7,33 +11,47 @@ namespace MediaBrowser.Controller.Subtitles { public class SubtitleSearchRequest : IHasProviderIds { + public SubtitleSearchRequest() + { + SearchAllProviders = true; + ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + + DisabledSubtitleFetchers = Array.Empty<string>(); + SubtitleFetcherOrder = Array.Empty<string>(); + } + public string Language { get; set; } + public string TwoLetterISOLanguageName { get; set; } public VideoContentType ContentType { get; set; } public string MediaPath { get; set; } + public string SeriesName { get; set; } + public string Name { get; set; } + public int? IndexNumber { get; set; } + public int? IndexNumberEnd { get; set; } + public int? ParentIndexNumber { get; set; } + public int? ProductionYear { get; set; } + public long? RuntimeTicks { get; set; } + public bool IsPerfectMatch { get; set; } + public Dictionary<string, string> ProviderIds { get; set; } public bool SearchAllProviders { get; set; } + public string[] DisabledSubtitleFetchers { get; set; } - public string[] SubtitleFetcherOrder { get; set; } - public SubtitleSearchRequest() - { - SearchAllProviders = true; - ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + public string[] SubtitleFetcherOrder { get; set; } - DisabledSubtitleFetchers = Array.Empty<string>(); - SubtitleFetcherOrder = Array.Empty<string>(); - } + public bool IsAutomated { get; set; } } } diff --git a/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs b/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs deleted file mode 100644 index d6bac23be..000000000 --- a/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Sync; - -namespace MediaBrowser.Controller.Sync -{ - public interface IHasDynamicAccess - { - /// <summary> - /// Gets the synced file information. - /// </summary> - /// <param name="id">The identifier.</param> - /// <param name="target">The target.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task<SyncedFileInfo>.</returns> - Task<SyncedFileInfo> GetSyncedFileInfo(string id, SyncTarget target, CancellationToken cancellationToken); - } -} diff --git a/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs b/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs deleted file mode 100644 index c0b62b753..000000000 --- a/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MediaBrowser.Controller.Sync -{ - /// <summary> - /// A marker interface - /// </summary> - public interface IRemoteSyncProvider - { - } -} diff --git a/MediaBrowser.Controller/Sync/IServerSyncProvider.cs b/MediaBrowser.Controller/Sync/IServerSyncProvider.cs deleted file mode 100644 index 8b2d5d779..000000000 --- a/MediaBrowser.Controller/Sync/IServerSyncProvider.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Sync; - -namespace MediaBrowser.Controller.Sync -{ - public interface IServerSyncProvider : ISyncProvider - { - /// <summary> - /// Transfers the file. - /// </summary> - Task<SyncedFileInfo> SendFile(SyncJob syncJob, string originalMediaPath, Stream inputStream, bool isMedia, string[] outputPathParts, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken); - - Task<QueryResult<FileSystemMetadata>> GetFiles(string[] directoryPathParts, SyncTarget target, CancellationToken cancellationToken); - } - - public interface ISupportsDirectCopy - { - /// <summary> - /// Sends the file. - /// </summary> - Task<SyncedFileInfo> SendFile(SyncJob syncJob, string originalMediaPath, string inputPath, bool isMedia, string[] outputPathParts, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken); - } -} diff --git a/MediaBrowser.Controller/Sync/ISyncProvider.cs b/MediaBrowser.Controller/Sync/ISyncProvider.cs deleted file mode 100644 index 56f6f3729..000000000 --- a/MediaBrowser.Controller/Sync/ISyncProvider.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using MediaBrowser.Model.Sync; - -namespace MediaBrowser.Controller.Sync -{ - public interface ISyncProvider - { - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - string Name { get; } - - /// <summary> - /// Gets the synchronize targets. - /// </summary> - /// <param name="userId">The user identifier.</param> - /// <returns>IEnumerable<SyncTarget>.</returns> - List<SyncTarget> GetSyncTargets(string userId); - - /// <summary> - /// Gets all synchronize targets. - /// </summary> - /// <returns>IEnumerable<SyncTarget>.</returns> - List<SyncTarget> GetAllSyncTargets(); - } -} diff --git a/MediaBrowser.Controller/Sync/SyncedFileInfo.cs b/MediaBrowser.Controller/Sync/SyncedFileInfo.cs deleted file mode 100644 index 2ff40addb..000000000 --- a/MediaBrowser.Controller/Sync/SyncedFileInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using MediaBrowser.Model.MediaInfo; - -namespace MediaBrowser.Controller.Sync -{ - public class SyncedFileInfo - { - /// <summary> - /// Gets or sets the path. - /// </summary> - /// <value>The path.</value> - public string Path { get; set; } - public string[] PathParts { get; set; } - /// <summary> - /// Gets or sets the protocol. - /// </summary> - /// <value>The protocol.</value> - public MediaProtocol Protocol { get; set; } - /// <summary> - /// Gets or sets the required HTTP headers. - /// </summary> - /// <value>The required HTTP headers.</value> - public Dictionary<string, string> RequiredHttpHeaders { get; set; } - /// <summary> - /// Gets or sets the identifier. - /// </summary> - /// <value>The identifier.</value> - public string Id { get; set; } - - public SyncedFileInfo() - { - RequiredHttpHeaders = new Dictionary<string, string>(); - } - } -} diff --git a/MediaBrowser.Controller/SyncPlay/GroupMember.cs b/MediaBrowser.Controller/SyncPlay/GroupMember.cs new file mode 100644 index 000000000..b973672c4 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/GroupMember.cs @@ -0,0 +1,60 @@ +#nullable disable + +using System; +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// <summary> + /// Class GroupMember. + /// </summary> + public class GroupMember + { + /// <summary> + /// Initializes a new instance of the <see cref="GroupMember"/> class. + /// </summary> + /// <param name="session">The session.</param> + public GroupMember(SessionInfo session) + { + SessionId = session.Id; + UserId = session.UserId; + UserName = session.UserName; + } + + /// <summary> + /// Gets the identifier of the session. + /// </summary> + /// <value>The session identifier.</value> + public string SessionId { get; } + + /// <summary> + /// Gets the identifier of the user. + /// </summary> + /// <value>The user identifier.</value> + public Guid UserId { get; } + + /// <summary> + /// Gets the username. + /// </summary> + /// <value>The username.</value> + public string UserName { get; } + + /// <summary> + /// Gets or sets the ping, in milliseconds. + /// </summary> + /// <value>The ping.</value> + public long Ping { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this member is buffering. + /// </summary> + /// <value><c>true</c> if member is buffering; <c>false</c> otherwise.</value> + public bool IsBuffering { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this member is following group playback. + /// </summary> + /// <value><c>true</c> to ignore member on group wait; <c>false</c> if they're following group playback.</value> + public bool IgnoreGroupWait { get; set; } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs new file mode 100644 index 000000000..51c95a1bb --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs @@ -0,0 +1,233 @@ +#nullable disable + +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.SyncPlay.PlaybackRequests; +using MediaBrowser.Model.SyncPlay; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Controller.SyncPlay.GroupStates +{ + /// <summary> + /// Class AbstractGroupState. + /// </summary> + /// <remarks> + /// Class is not thread-safe, external locking is required when accessing methods. + /// </remarks> + public abstract class AbstractGroupState : IGroupState + { + /// <summary> + /// The logger. + /// </summary> + private readonly ILogger<AbstractGroupState> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="AbstractGroupState"/> class. + /// </summary> + /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param> + protected AbstractGroupState(ILoggerFactory loggerFactory) + { + LoggerFactory = loggerFactory; + _logger = loggerFactory.CreateLogger<AbstractGroupState>(); + } + + /// <inheritdoc /> + public abstract GroupStateType Type { get; } + + /// <summary> + /// Gets the logger factory. + /// </summary> + protected ILoggerFactory LoggerFactory { get; } + + /// <inheritdoc /> + public abstract void SessionJoined(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken); + + /// <inheritdoc /> + public abstract void SessionLeaving(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken); + + /// <inheritdoc /> + public virtual void HandleRequest(IGroupPlaybackRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + UnhandledRequest(request); + } + + /// <inheritdoc /> + public virtual void HandleRequest(PlayGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + UnhandledRequest(request); + } + + /// <inheritdoc /> + public virtual void HandleRequest(SetPlaylistItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + var waitingState = new WaitingGroupState(LoggerFactory); + context.SetState(waitingState); + waitingState.HandleRequest(request, context, Type, session, cancellationToken); + } + + /// <inheritdoc /> + public virtual void HandleRequest(RemoveFromPlaylistGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + bool playingItemRemoved; + if (request.ClearPlaylist) + { + context.ClearPlayQueue(request.ClearPlayingItem); + playingItemRemoved = request.ClearPlayingItem; + } + else + { + playingItemRemoved = context.RemoveFromPlayQueue(request.PlaylistItemIds); + } + + var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.RemoveItems); + var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); + + if (playingItemRemoved && !context.PlayQueue.IsItemPlaying()) + { + _logger.LogDebug("Play queue in group {GroupId} is now empty.", context.GroupId.ToString()); + + IGroupState idleState = new IdleGroupState(LoggerFactory); + context.SetState(idleState); + var stopRequest = new StopGroupRequest(); + idleState.HandleRequest(stopRequest, context, Type, session, cancellationToken); + } + } + + /// <inheritdoc /> + public virtual void HandleRequest(MovePlaylistItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + var result = context.MoveItemInPlayQueue(request.PlaylistItemId, request.NewIndex); + + if (!result) + { + _logger.LogError("Unable to move item in group {GroupId}.", context.GroupId.ToString()); + return; + } + + var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.MoveItem); + var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); + } + + /// <inheritdoc /> + public virtual void HandleRequest(QueueGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + var result = context.AddToPlayQueue(request.ItemIds, request.Mode); + + if (!result) + { + _logger.LogError("Unable to add items to play queue in group {GroupId}.", context.GroupId.ToString()); + return; + } + + var reason = request.Mode switch + { + GroupQueueMode.QueueNext => PlayQueueUpdateReason.QueueNext, + _ => PlayQueueUpdateReason.Queue + }; + var playQueueUpdate = context.GetPlayQueueUpdate(reason); + var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); + } + + /// <inheritdoc /> + public virtual void HandleRequest(UnpauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + UnhandledRequest(request); + } + + /// <inheritdoc /> + public virtual void HandleRequest(PauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + UnhandledRequest(request); + } + + /// <inheritdoc /> + public virtual void HandleRequest(StopGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + UnhandledRequest(request); + } + + /// <inheritdoc /> + public virtual void HandleRequest(SeekGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + UnhandledRequest(request); + } + + /// <inheritdoc /> + public virtual void HandleRequest(BufferGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + UnhandledRequest(request); + } + + /// <inheritdoc /> + public virtual void HandleRequest(ReadyGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + UnhandledRequest(request); + } + + /// <inheritdoc /> + public virtual void HandleRequest(NextItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + UnhandledRequest(request); + } + + /// <inheritdoc /> + public virtual void HandleRequest(PreviousItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + UnhandledRequest(request); + } + + /// <inheritdoc /> + public virtual void HandleRequest(SetRepeatModeGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + context.SetRepeatMode(request.Mode); + var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.RepeatMode); + var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); + } + + /// <inheritdoc /> + public virtual void HandleRequest(SetShuffleModeGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + context.SetShuffleMode(request.Mode); + var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.ShuffleMode); + var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); + } + + /// <inheritdoc /> + public virtual void HandleRequest(PingGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Collected pings are used to account for network latency when unpausing playback. + context.UpdatePing(session, request.Ping); + } + + /// <inheritdoc /> + public virtual void HandleRequest(IgnoreWaitGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + context.SetIgnoreGroupWait(session, request.IgnoreWait); + } + + /// <summary> + /// Sends a group state update to all group. + /// </summary> + /// <param name="context">The context of the state.</param> + /// <param name="reason">The reason of the state change.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + protected void SendGroupStateUpdate(IGroupStateContext context, IGroupPlaybackRequest reason, SessionInfo session, CancellationToken cancellationToken) + { + // Notify relevant state change event. + var stateUpdate = new GroupStateUpdate(Type, reason.Action); + var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.StateUpdate, stateUpdate); + context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); + } + + private void UnhandledRequest(IGroupPlaybackRequest request) + { + _logger.LogWarning("Unhandled request of type {RequestType} in {StateType} state.", request.Action, Type); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/IdleGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/IdleGroupState.cs new file mode 100644 index 000000000..6b5a7cdf9 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/GroupStates/IdleGroupState.cs @@ -0,0 +1,128 @@ +#nullable disable + +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.SyncPlay.PlaybackRequests; +using MediaBrowser.Model.SyncPlay; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Controller.SyncPlay.GroupStates +{ + /// <summary> + /// Class IdleGroupState. + /// </summary> + /// <remarks> + /// Class is not thread-safe, external locking is required when accessing methods. + /// </remarks> + public class IdleGroupState : AbstractGroupState + { + /// <summary> + /// The logger. + /// </summary> + private readonly ILogger<IdleGroupState> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="IdleGroupState"/> class. + /// </summary> + /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param> + public IdleGroupState(ILoggerFactory loggerFactory) + : base(loggerFactory) + { + _logger = LoggerFactory.CreateLogger<IdleGroupState>(); + } + + /// <inheritdoc /> + public override GroupStateType Type { get; } = GroupStateType.Idle; + + /// <inheritdoc /> + public override void SessionJoined(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + SendStopCommand(context, Type, session, cancellationToken); + } + + /// <inheritdoc /> + public override void SessionLeaving(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Do nothing. + } + + /// <inheritdoc /> + public override void HandleRequest(PlayGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Change state. + var waitingState = new WaitingGroupState(LoggerFactory); + context.SetState(waitingState); + waitingState.HandleRequest(request, context, Type, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(UnpauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Change state. + var waitingState = new WaitingGroupState(LoggerFactory); + context.SetState(waitingState); + waitingState.HandleRequest(request, context, Type, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(PauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + SendStopCommand(context, prevState, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(StopGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + SendStopCommand(context, prevState, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(SeekGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + SendStopCommand(context, prevState, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(BufferGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + SendStopCommand(context, prevState, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(ReadyGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + SendStopCommand(context, prevState, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(NextItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Change state. + var waitingState = new WaitingGroupState(LoggerFactory); + context.SetState(waitingState); + waitingState.HandleRequest(request, context, Type, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(PreviousItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Change state. + var waitingState = new WaitingGroupState(LoggerFactory); + context.SetState(waitingState); + waitingState.HandleRequest(request, context, Type, session, cancellationToken); + } + + private void SendStopCommand(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + var command = context.NewSyncPlayCommand(SendCommandType.Stop); + if (!prevState.Equals(Type)) + { + context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken); + } + else + { + context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken); + } + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/PausedGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/PausedGroupState.cs new file mode 100644 index 000000000..b9786ddb0 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/GroupStates/PausedGroupState.cs @@ -0,0 +1,167 @@ +#nullable disable + +using System; +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.SyncPlay.PlaybackRequests; +using MediaBrowser.Model.SyncPlay; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Controller.SyncPlay.GroupStates +{ + /// <summary> + /// Class PausedGroupState. + /// </summary> + /// <remarks> + /// Class is not thread-safe, external locking is required when accessing methods. + /// </remarks> + public class PausedGroupState : AbstractGroupState + { + /// <summary> + /// The logger. + /// </summary> + private readonly ILogger<PausedGroupState> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="PausedGroupState"/> class. + /// </summary> + /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param> + public PausedGroupState(ILoggerFactory loggerFactory) + : base(loggerFactory) + { + _logger = LoggerFactory.CreateLogger<PausedGroupState>(); + } + + /// <inheritdoc /> + public override GroupStateType Type { get; } = GroupStateType.Paused; + + /// <inheritdoc /> + public override void SessionJoined(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Wait for session to be ready. + var waitingState = new WaitingGroupState(LoggerFactory); + context.SetState(waitingState); + waitingState.SessionJoined(context, Type, session, cancellationToken); + } + + /// <inheritdoc /> + public override void SessionLeaving(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Do nothing. + } + + /// <inheritdoc /> + public override void HandleRequest(PlayGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Change state. + var waitingState = new WaitingGroupState(LoggerFactory); + context.SetState(waitingState); + waitingState.HandleRequest(request, context, Type, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(UnpauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Change state. + var playingState = new PlayingGroupState(LoggerFactory); + context.SetState(playingState); + playingState.HandleRequest(request, context, Type, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(PauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + if (!prevState.Equals(Type)) + { + // Pause group and compute the media playback position. + var currentTime = DateTime.UtcNow; + var elapsedTime = currentTime - context.LastActivity; + context.LastActivity = currentTime; + // Elapsed time is negative if event happens + // during the delay added to account for latency. + // In this phase clients haven't started the playback yet. + // In other words, LastActivity is in the future, + // when playback unpause is supposed to happen. + // Seek only if playback actually started. + context.PositionTicks += Math.Max(elapsedTime.Ticks, 0); + + var command = context.NewSyncPlayCommand(SendCommandType.Pause); + context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken); + + // Notify relevant state change event. + SendGroupStateUpdate(context, request, session, cancellationToken); + } + else + { + // Client got lost, sending current state. + var command = context.NewSyncPlayCommand(SendCommandType.Pause); + context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken); + } + } + + /// <inheritdoc /> + public override void HandleRequest(StopGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Change state. + var idleState = new IdleGroupState(LoggerFactory); + context.SetState(idleState); + idleState.HandleRequest(request, context, Type, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(SeekGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Change state. + var waitingState = new WaitingGroupState(LoggerFactory); + context.SetState(waitingState); + waitingState.HandleRequest(request, context, Type, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(BufferGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Change state. + var waitingState = new WaitingGroupState(LoggerFactory); + context.SetState(waitingState); + waitingState.HandleRequest(request, context, Type, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(ReadyGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + if (prevState.Equals(Type)) + { + // Client got lost, sending current state. + var command = context.NewSyncPlayCommand(SendCommandType.Pause); + context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken); + } + else if (prevState.Equals(GroupStateType.Waiting)) + { + // Sending current state to all clients. + var command = context.NewSyncPlayCommand(SendCommandType.Pause); + context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken); + + // Notify relevant state change event. + SendGroupStateUpdate(context, request, session, cancellationToken); + } + } + + /// <inheritdoc /> + public override void HandleRequest(NextItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Change state. + var waitingState = new WaitingGroupState(LoggerFactory); + context.SetState(waitingState); + waitingState.HandleRequest(request, context, Type, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(PreviousItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Change state. + var waitingState = new WaitingGroupState(LoggerFactory); + context.SetState(waitingState); + waitingState.HandleRequest(request, context, Type, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/PlayingGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/PlayingGroupState.cs new file mode 100644 index 000000000..cb1cadf0b --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/GroupStates/PlayingGroupState.cs @@ -0,0 +1,170 @@ +#nullable disable + +using System; +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.SyncPlay.PlaybackRequests; +using MediaBrowser.Model.SyncPlay; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Controller.SyncPlay.GroupStates +{ + /// <summary> + /// Class PlayingGroupState. + /// </summary> + /// <remarks> + /// Class is not thread-safe, external locking is required when accessing methods. + /// </remarks> + public class PlayingGroupState : AbstractGroupState + { + /// <summary> + /// The logger. + /// </summary> + private readonly ILogger<PlayingGroupState> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="PlayingGroupState"/> class. + /// </summary> + /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param> + public PlayingGroupState(ILoggerFactory loggerFactory) + : base(loggerFactory) + { + _logger = LoggerFactory.CreateLogger<PlayingGroupState>(); + } + + /// <inheritdoc /> + public override GroupStateType Type { get; } = GroupStateType.Playing; + + /// <summary> + /// Gets or sets a value indicating whether requests for buffering should be ignored. + /// </summary> + public bool IgnoreBuffering { get; set; } + + /// <inheritdoc /> + public override void SessionJoined(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Wait for session to be ready. + var waitingState = new WaitingGroupState(LoggerFactory); + context.SetState(waitingState); + waitingState.SessionJoined(context, Type, session, cancellationToken); + } + + /// <inheritdoc /> + public override void SessionLeaving(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Do nothing. + } + + /// <inheritdoc /> + public override void HandleRequest(PlayGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Change state. + var waitingState = new WaitingGroupState(LoggerFactory); + context.SetState(waitingState); + waitingState.HandleRequest(request, context, Type, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(UnpauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + if (!prevState.Equals(Type)) + { + // Pick a suitable time that accounts for latency. + var delayMillis = Math.Max(context.GetHighestPing() * 2, context.DefaultPing); + + // Unpause group and set starting point in future. + // Clients will start playback at LastActivity (datetime) from PositionTicks (playback position). + // The added delay does not guarantee, of course, that the command will be received in time. + // Playback synchronization will mainly happen client side. + context.LastActivity = DateTime.UtcNow.AddMilliseconds(delayMillis); + + var command = context.NewSyncPlayCommand(SendCommandType.Unpause); + context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken); + + // Notify relevant state change event. + SendGroupStateUpdate(context, request, session, cancellationToken); + } + else + { + // Client got lost, sending current state. + var command = context.NewSyncPlayCommand(SendCommandType.Unpause); + context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken); + } + } + + /// <inheritdoc /> + public override void HandleRequest(PauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Change state. + var pausedState = new PausedGroupState(LoggerFactory); + context.SetState(pausedState); + pausedState.HandleRequest(request, context, Type, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(StopGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Change state. + var idleState = new IdleGroupState(LoggerFactory); + context.SetState(idleState); + idleState.HandleRequest(request, context, Type, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(SeekGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Change state. + var waitingState = new WaitingGroupState(LoggerFactory); + context.SetState(waitingState); + waitingState.HandleRequest(request, context, Type, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(BufferGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + if (IgnoreBuffering) + { + return; + } + + // Change state. + var waitingState = new WaitingGroupState(LoggerFactory); + context.SetState(waitingState); + waitingState.HandleRequest(request, context, Type, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(ReadyGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + if (prevState.Equals(Type)) + { + // Group was not waiting, make sure client has latest state. + var command = context.NewSyncPlayCommand(SendCommandType.Unpause); + context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken); + } + else if (prevState.Equals(GroupStateType.Waiting)) + { + // Notify relevant state change event. + SendGroupStateUpdate(context, request, session, cancellationToken); + } + } + + /// <inheritdoc /> + public override void HandleRequest(NextItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Change state. + var waitingState = new WaitingGroupState(LoggerFactory); + context.SetState(waitingState); + waitingState.HandleRequest(request, context, Type, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(PreviousItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Change state. + var waitingState = new WaitingGroupState(LoggerFactory); + context.SetState(waitingState); + waitingState.HandleRequest(request, context, Type, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs new file mode 100644 index 000000000..a0c38b309 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs @@ -0,0 +1,682 @@ +#nullable disable + +using System; +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.SyncPlay.PlaybackRequests; +using MediaBrowser.Model.SyncPlay; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Controller.SyncPlay.GroupStates +{ + /// <summary> + /// Class WaitingGroupState. + /// </summary> + /// <remarks> + /// Class is not thread-safe, external locking is required when accessing methods. + /// </remarks> + public class WaitingGroupState : AbstractGroupState + { + /// <summary> + /// The logger. + /// </summary> + private readonly ILogger<WaitingGroupState> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="WaitingGroupState"/> class. + /// </summary> + /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param> + public WaitingGroupState(ILoggerFactory loggerFactory) + : base(loggerFactory) + { + _logger = LoggerFactory.CreateLogger<WaitingGroupState>(); + } + + /// <inheritdoc /> + public override GroupStateType Type { get; } = GroupStateType.Waiting; + + /// <summary> + /// Gets or sets a value indicating whether playback should resume when group is ready. + /// </summary> + public bool ResumePlaying { get; set; } = false; + + /// <summary> + /// Gets or sets a value indicating whether the initial state has been set. + /// </summary> + private bool InitialStateSet { get; set; } = false; + + /// <summary> + /// Gets or sets the group state before the first ever event. + /// </summary> + private GroupStateType InitialState { get; set; } + + /// <inheritdoc /> + public override void SessionJoined(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Save state if first event. + if (!InitialStateSet) + { + InitialState = prevState; + InitialStateSet = true; + } + + if (prevState.Equals(GroupStateType.Playing)) + { + ResumePlaying = true; + // Pause group and compute the media playback position. + var currentTime = DateTime.UtcNow; + var elapsedTime = currentTime - context.LastActivity; + context.LastActivity = currentTime; + // Elapsed time is negative if event happens + // during the delay added to account for latency. + // In this phase clients haven't started the playback yet. + // In other words, LastActivity is in the future, + // when playback unpause is supposed to happen. + // Seek only if playback actually started. + context.PositionTicks += Math.Max(elapsedTime.Ticks, 0); + } + + // Prepare new session. + var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NewPlaylist); + var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + context.SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, update, cancellationToken); + + context.SetBuffering(session, true); + + // Send pause command to all non-buffering sessions. + var command = context.NewSyncPlayCommand(SendCommandType.Pause); + context.SendCommand(session, SyncPlayBroadcastType.AllReady, command, cancellationToken); + } + + /// <inheritdoc /> + public override void SessionLeaving(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Save state if first event. + if (!InitialStateSet) + { + InitialState = prevState; + InitialStateSet = true; + } + + context.SetBuffering(session, false); + + if (!context.IsBuffering()) + { + if (ResumePlaying) + { + _logger.LogDebug("Session {SessionId} left group {GroupId}, notifying others to resume.", session.Id, context.GroupId.ToString()); + + // Client, that was buffering, left the group. + var playingState = new PlayingGroupState(LoggerFactory); + context.SetState(playingState); + var unpauseRequest = new UnpauseGroupRequest(); + playingState.HandleRequest(unpauseRequest, context, Type, session, cancellationToken); + } + else + { + _logger.LogDebug("Session {SessionId} left group {GroupId}, returning to previous state.", session.Id, context.GroupId.ToString()); + + // Group is ready, returning to previous state. + var pausedState = new PausedGroupState(LoggerFactory); + context.SetState(pausedState); + } + } + } + + /// <inheritdoc /> + public override void HandleRequest(PlayGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Save state if first event. + if (!InitialStateSet) + { + InitialState = prevState; + InitialStateSet = true; + } + + ResumePlaying = true; + + var setQueueStatus = context.SetPlayQueue(request.PlayingQueue, request.PlayingItemPosition, request.StartPositionTicks); + if (!setQueueStatus) + { + _logger.LogError("Unable to set playing queue in group {GroupId}.", context.GroupId.ToString()); + + // Ignore request and return to previous state. + IGroupState newState = prevState switch { + GroupStateType.Playing => new PlayingGroupState(LoggerFactory), + GroupStateType.Paused => new PausedGroupState(LoggerFactory), + _ => new IdleGroupState(LoggerFactory) + }; + + context.SetState(newState); + return; + } + + var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NewPlaylist); + var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); + + // Reset status of sessions and await for all Ready events. + context.SetAllBuffering(true); + + _logger.LogDebug("Session {SessionId} set a new play queue in group {GroupId}.", session.Id, context.GroupId.ToString()); + } + + /// <inheritdoc /> + public override void HandleRequest(SetPlaylistItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Save state if first event. + if (!InitialStateSet) + { + InitialState = prevState; + InitialStateSet = true; + } + + ResumePlaying = true; + + var result = context.SetPlayingItem(request.PlaylistItemId); + if (result) + { + var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.SetCurrentItem); + var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); + + // Reset status of sessions and await for all Ready events. + context.SetAllBuffering(true); + } + else + { + // Return to old state. + IGroupState newState = prevState switch + { + GroupStateType.Playing => new PlayingGroupState(LoggerFactory), + GroupStateType.Paused => new PausedGroupState(LoggerFactory), + _ => new IdleGroupState(LoggerFactory) + }; + + context.SetState(newState); + + _logger.LogDebug("Unable to change current playing item in group {GroupId}.", context.GroupId.ToString()); + } + } + + /// <inheritdoc /> + public override void HandleRequest(UnpauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Save state if first event. + if (!InitialStateSet) + { + InitialState = prevState; + InitialStateSet = true; + } + + if (prevState.Equals(GroupStateType.Idle)) + { + ResumePlaying = true; + context.RestartCurrentItem(); + + var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NewPlaylist); + var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); + + // Reset status of sessions and await for all Ready events. + context.SetAllBuffering(true); + + _logger.LogDebug("Group {GroupId} is waiting for all ready events.", context.GroupId.ToString()); + } + else + { + if (ResumePlaying) + { + _logger.LogDebug("Forcing the playback to start in group {GroupId}. Group-wait is disabled until next state change.", context.GroupId.ToString()); + + // An Unpause request is forcing the playback to start, ignoring sessions that are not ready. + context.SetAllBuffering(false); + + // Change state. + var playingState = new PlayingGroupState(LoggerFactory) + { + IgnoreBuffering = true + }; + context.SetState(playingState); + playingState.HandleRequest(request, context, Type, session, cancellationToken); + } + else + { + // Group would have gone to paused state, now will go to playing state when ready. + ResumePlaying = true; + + // Notify relevant state change event. + SendGroupStateUpdate(context, request, session, cancellationToken); + } + } + } + + /// <inheritdoc /> + public override void HandleRequest(PauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Save state if first event. + if (!InitialStateSet) + { + InitialState = prevState; + InitialStateSet = true; + } + + // Wait for sessions to be ready, then switch to paused state. + ResumePlaying = false; + + // Notify relevant state change event. + SendGroupStateUpdate(context, request, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(StopGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Save state if first event. + if (!InitialStateSet) + { + InitialState = prevState; + InitialStateSet = true; + } + + // Change state. + var idleState = new IdleGroupState(LoggerFactory); + context.SetState(idleState); + idleState.HandleRequest(request, context, Type, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(SeekGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Save state if first event. + if (!InitialStateSet) + { + InitialState = prevState; + InitialStateSet = true; + } + + if (prevState.Equals(GroupStateType.Playing)) + { + ResumePlaying = true; + } + else if (prevState.Equals(GroupStateType.Paused)) + { + ResumePlaying = false; + } + + // Sanitize PositionTicks. + var ticks = context.SanitizePositionTicks(request.PositionTicks); + + // Seek. + context.PositionTicks = ticks; + context.LastActivity = DateTime.UtcNow; + + var command = context.NewSyncPlayCommand(SendCommandType.Seek); + context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken); + + // Reset status of sessions and await for all Ready events. + context.SetAllBuffering(true); + + // Notify relevant state change event. + SendGroupStateUpdate(context, request, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(BufferGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Save state if first event. + if (!InitialStateSet) + { + InitialState = prevState; + InitialStateSet = true; + } + + // Make sure the client is playing the correct item. + if (!request.PlaylistItemId.Equals(context.PlayQueue.GetPlayingItemPlaylistId())) + { + _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); + context.SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken); + context.SetBuffering(session, true); + + return; + } + + if (prevState.Equals(GroupStateType.Playing)) + { + // Resume playback when all ready. + ResumePlaying = true; + + context.SetBuffering(session, true); + + // Pause group and compute the media playback position. + var currentTime = DateTime.UtcNow; + var elapsedTime = currentTime - context.LastActivity; + context.LastActivity = currentTime; + // Elapsed time is negative if event happens + // during the delay added to account for latency. + // In this phase clients haven't started the playback yet. + // In other words, LastActivity is in the future, + // when playback unpause is supposed to happen. + // Seek only if playback actually started. + context.PositionTicks += Math.Max(elapsedTime.Ticks, 0); + + // Send pause command to all non-buffering sessions. + var command = context.NewSyncPlayCommand(SendCommandType.Pause); + context.SendCommand(session, SyncPlayBroadcastType.AllReady, command, cancellationToken); + } + else if (prevState.Equals(GroupStateType.Paused)) + { + // Don't resume playback when all ready. + ResumePlaying = false; + + context.SetBuffering(session, true); + + // Send pause command to buffering session. + var command = context.NewSyncPlayCommand(SendCommandType.Pause); + context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken); + } + else if (prevState.Equals(GroupStateType.Waiting)) + { + // Another session is now buffering. + context.SetBuffering(session, true); + + if (!ResumePlaying) + { + // Force update for this session that should be paused. + var command = context.NewSyncPlayCommand(SendCommandType.Pause); + context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken); + } + } + + // Notify relevant state change event. + SendGroupStateUpdate(context, request, session, cancellationToken); + } + + /// <inheritdoc /> + public override void HandleRequest(ReadyGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Save state if first event. + if (!InitialStateSet) + { + InitialState = prevState; + InitialStateSet = true; + } + + // Make sure the client is playing the correct item. + if (!request.PlaylistItemId.Equals(context.PlayQueue.GetPlayingItemPlaylistId())) + { + _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); + context.SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, update, cancellationToken); + context.SetBuffering(session, true); + + return; + } + + // Compute elapsed time between the client reported time and now. + // Elapsed time is used to estimate the client position when playback is unpaused. + // Ideally, the request is received and handled without major delays. + // However, to avoid waiting indefinitely when a client is not reporting a correct time, + // the elapsed time is ignored after a certain threshold. + var currentTime = DateTime.UtcNow; + var elapsedTime = currentTime.Subtract(request.When); + var timeSyncThresholdTicks = TimeSpan.FromMilliseconds(context.TimeSyncOffset).Ticks; + if (Math.Abs(elapsedTime.Ticks) > timeSyncThresholdTicks) + { + _logger.LogWarning("Session {SessionId} is not time syncing properly. Ignoring elapsed time.", session.Id); + + elapsedTime = TimeSpan.Zero; + } + + // Ignore elapsed time if client is paused. + if (!request.IsPlaying) + { + elapsedTime = TimeSpan.Zero; + } + + var requestTicks = context.SanitizePositionTicks(request.PositionTicks); + var clientPosition = TimeSpan.FromTicks(requestTicks) + elapsedTime; + var delayTicks = context.PositionTicks - clientPosition.Ticks; + var maxPlaybackOffsetTicks = TimeSpan.FromMilliseconds(context.MaxPlaybackOffset).Ticks; + + _logger.LogDebug("Session {SessionId} is at {PositionTicks} (delay of {Delay} seconds) in group {GroupId}.", session.Id, clientPosition, TimeSpan.FromTicks(delayTicks).TotalSeconds, context.GroupId.ToString()); + + if (ResumePlaying) + { + // Handle case where session reported as ready but in reality + // it has no clue of the real position nor the playback state. + if (!request.IsPlaying && Math.Abs(delayTicks) > maxPlaybackOffsetTicks) + { + // Session not ready at all. + context.SetBuffering(session, true); + + // Correcting session's position. + var command = context.NewSyncPlayCommand(SendCommandType.Seek); + context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken); + + // Notify relevant state change event. + SendGroupStateUpdate(context, request, session, cancellationToken); + + _logger.LogWarning("Session {SessionId} got lost in time, correcting.", session.Id); + return; + } + + // Session is ready. + context.SetBuffering(session, false); + + if (context.IsBuffering()) + { + // Others are still buffering, tell this client to pause when ready. + var command = context.NewSyncPlayCommand(SendCommandType.Pause); + command.When = currentTime.AddTicks(delayTicks); + context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken); + + _logger.LogInformation("Session {SessionId} will pause when ready in {Delay} seconds. Group {GroupId} is waiting for all ready events.", session.Id, TimeSpan.FromTicks(delayTicks).TotalSeconds, context.GroupId.ToString()); + } + else + { + // If all ready, then start playback. + // Let other clients resume as soon as the buffering client catches up. + if (delayTicks > context.GetHighestPing() * 2 * TimeSpan.TicksPerMillisecond) + { + // Client that was buffering is recovering, notifying others to resume. + context.LastActivity = currentTime.AddTicks(delayTicks); + var command = context.NewSyncPlayCommand(SendCommandType.Unpause); + var filter = SyncPlayBroadcastType.AllExceptCurrentSession; + if (!request.IsPlaying) + { + filter = SyncPlayBroadcastType.AllGroup; + } + + context.SendCommand(session, filter, command, cancellationToken); + + _logger.LogInformation("Session {SessionId} is recovering, group {GroupId} will resume in {Delay} seconds.", session.Id, context.GroupId.ToString(), TimeSpan.FromTicks(delayTicks).TotalSeconds); + } + else + { + // Client, that was buffering, resumed playback but did not update others in time. + delayTicks = context.GetHighestPing() * 2 * TimeSpan.TicksPerMillisecond; + delayTicks = Math.Max(delayTicks, context.DefaultPing); + + context.LastActivity = currentTime.AddTicks(delayTicks); + + var command = context.NewSyncPlayCommand(SendCommandType.Unpause); + context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken); + + _logger.LogWarning("Session {SessionId} resumed playback, group {GroupId} has {Delay} seconds to recover.", session.Id, context.GroupId.ToString(), TimeSpan.FromTicks(delayTicks).TotalSeconds); + } + + // Change state. + var playingState = new PlayingGroupState(LoggerFactory); + context.SetState(playingState); + playingState.HandleRequest(request, context, Type, session, cancellationToken); + } + } + else + { + // Check that session is really ready, tolerate player imperfections under a certain threshold. + if (Math.Abs(context.PositionTicks - requestTicks) > maxPlaybackOffsetTicks) + { + // Session still not ready. + context.SetBuffering(session, true); + // Session is seeking to wrong position, correcting. + var command = context.NewSyncPlayCommand(SendCommandType.Seek); + context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken); + + // Notify relevant state change event. + SendGroupStateUpdate(context, request, session, cancellationToken); + + _logger.LogWarning("Session {SessionId} is seeking to wrong position, correcting.", session.Id); + return; + } + else + { + // Session is ready. + context.SetBuffering(session, false); + } + + if (!context.IsBuffering()) + { + _logger.LogDebug("Session {SessionId} is ready, group {GroupId} is ready.", session.Id, context.GroupId.ToString()); + + // Group is ready, returning to previous state. + var pausedState = new PausedGroupState(LoggerFactory); + context.SetState(pausedState); + + if (InitialState.Equals(GroupStateType.Playing)) + { + // Group went from playing to waiting state and a pause request occured while waiting. + var pauseRequest = new PauseGroupRequest(); + pausedState.HandleRequest(pauseRequest, context, Type, session, cancellationToken); + } + else if (InitialState.Equals(GroupStateType.Paused)) + { + pausedState.HandleRequest(request, context, Type, session, cancellationToken); + } + } + } + } + + /// <inheritdoc /> + public override void HandleRequest(NextItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Save state if first event. + if (!InitialStateSet) + { + InitialState = prevState; + InitialStateSet = true; + } + + ResumePlaying = true; + + // Make sure the client knows the playing item, to avoid duplicate requests. + if (!request.PlaylistItemId.Equals(context.PlayQueue.GetPlayingItemPlaylistId())) + { + _logger.LogDebug("Session {SessionId} provided the wrong playlist item for group {GroupId}.", session.Id, context.GroupId.ToString()); + return; + } + + var newItem = context.NextItemInQueue(); + if (newItem) + { + // Send playing-queue update. + var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NextItem); + var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); + + // Reset status of sessions and await for all Ready events. + context.SetAllBuffering(true); + } + else + { + // Return to old state. + IGroupState newState = prevState switch + { + GroupStateType.Playing => new PlayingGroupState(LoggerFactory), + GroupStateType.Paused => new PausedGroupState(LoggerFactory), + _ => new IdleGroupState(LoggerFactory) + }; + + context.SetState(newState); + + _logger.LogDebug("No next item available in group {GroupId}.", context.GroupId.ToString()); + } + } + + /// <inheritdoc /> + public override void HandleRequest(PreviousItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + // Save state if first event. + if (!InitialStateSet) + { + InitialState = prevState; + InitialStateSet = true; + } + + ResumePlaying = true; + + // Make sure the client knows the playing item, to avoid duplicate requests. + if (!request.PlaylistItemId.Equals(context.PlayQueue.GetPlayingItemPlaylistId())) + { + _logger.LogDebug("Session {SessionId} provided the wrong playlist item for group {GroupId}.", session.Id, context.GroupId.ToString()); + return; + } + + var newItem = context.PreviousItemInQueue(); + if (newItem) + { + // Send playing-queue update. + var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.PreviousItem); + var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); + + // Reset status of sessions and await for all Ready events. + context.SetAllBuffering(true); + } + else + { + // Return to old state. + IGroupState newState = prevState switch + { + GroupStateType.Playing => new PlayingGroupState(LoggerFactory), + GroupStateType.Paused => new PausedGroupState(LoggerFactory), + _ => new IdleGroupState(LoggerFactory) + }; + + context.SetState(newState); + + _logger.LogDebug("No previous item available in group {GroupId}.", context.GroupId.ToString()); + } + } + + /// <inheritdoc /> + public override void HandleRequest(IgnoreWaitGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken) + { + context.SetIgnoreGroupWait(session, request.IgnoreWait); + + if (!context.IsBuffering()) + { + _logger.LogDebug("Ignoring session {SessionId}, group {GroupId} is ready.", session.Id, context.GroupId.ToString()); + + if (ResumePlaying) + { + // Client, that was buffering, stopped following playback. + var playingState = new PlayingGroupState(LoggerFactory); + context.SetState(playingState); + var unpauseRequest = new UnpauseGroupRequest(); + playingState.HandleRequest(unpauseRequest, context, Type, session, cancellationToken); + } + else + { + // Group is ready, returning to previous state. + var pausedState = new PausedGroupState(LoggerFactory); + context.SetState(pausedState); + } + } + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/IGroupPlaybackRequest.cs b/MediaBrowser.Controller/SyncPlay/IGroupPlaybackRequest.cs new file mode 100644 index 000000000..9045063ee --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/IGroupPlaybackRequest.cs @@ -0,0 +1,29 @@ +#nullable disable + +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// <summary> + /// Interface IGroupPlaybackRequest. + /// </summary> + public interface IGroupPlaybackRequest : ISyncPlayRequest + { + /// <summary> + /// Gets the playback request type. + /// </summary> + /// <returns>The playback request type.</returns> + PlaybackRequestType Action { get; } + + /// <summary> + /// Applies the request to a group. + /// </summary> + /// <param name="context">The context of the state.</param> + /// <param name="state">The current state.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/SyncPlay/IGroupState.cs b/MediaBrowser.Controller/SyncPlay/IGroupState.cs new file mode 100644 index 000000000..0666a62a8 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/IGroupState.cs @@ -0,0 +1,219 @@ +#nullable disable + +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.SyncPlay.PlaybackRequests; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// <summary> + /// Interface IGroupState. + /// </summary> + public interface IGroupState + { + /// <summary> + /// Gets the group state type. + /// </summary> + /// <value>The group state type.</value> + GroupStateType Type { get; } + + /// <summary> + /// Handles a session that joined the group. + /// </summary> + /// <param name="context">The context of the state.</param> + /// <param name="prevState">The previous state.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void SessionJoined(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Handles a session that is leaving the group. + /// </summary> + /// <param name="context">The context of the state.</param> + /// <param name="prevState">The previous state.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void SessionLeaving(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Generic handler. Context's state can change. + /// </summary> + /// <param name="request">The generic request.</param> + /// <param name="context">The context of the state.</param> + /// <param name="prevState">The previous state.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void HandleRequest(IGroupPlaybackRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Handles a play request from a session. Context's state can change. + /// </summary> + /// <param name="request">The play request.</param> + /// <param name="context">The context of the state.</param> + /// <param name="prevState">The previous state.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void HandleRequest(PlayGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Handles a set-playlist-item request from a session. Context's state can change. + /// </summary> + /// <param name="request">The set-playlist-item request.</param> + /// <param name="context">The context of the state.</param> + /// <param name="prevState">The previous state.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void HandleRequest(SetPlaylistItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Handles a remove-items request from a session. Context's state can change. + /// </summary> + /// <param name="request">The remove-items request.</param> + /// <param name="context">The context of the state.</param> + /// <param name="prevState">The previous state.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void HandleRequest(RemoveFromPlaylistGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Handles a move-playlist-item request from a session. Context's state should not change. + /// </summary> + /// <param name="request">The move-playlist-item request.</param> + /// <param name="context">The context of the state.</param> + /// <param name="prevState">The previous state.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void HandleRequest(MovePlaylistItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Handles a queue request from a session. Context's state should not change. + /// </summary> + /// <param name="request">The queue request.</param> + /// <param name="context">The context of the state.</param> + /// <param name="prevState">The previous state.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void HandleRequest(QueueGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Handles an unpause request from a session. Context's state can change. + /// </summary> + /// <param name="request">The unpause request.</param> + /// <param name="context">The context of the state.</param> + /// <param name="prevState">The previous state.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void HandleRequest(UnpauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Handles a pause request from a session. Context's state can change. + /// </summary> + /// <param name="request">The pause request.</param> + /// <param name="context">The context of the state.</param> + /// <param name="prevState">The previous state.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void HandleRequest(PauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Handles a stop request from a session. Context's state can change. + /// </summary> + /// <param name="request">The stop request.</param> + /// <param name="context">The context of the state.</param> + /// <param name="prevState">The previous state.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void HandleRequest(StopGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Handles a seek request from a session. Context's state can change. + /// </summary> + /// <param name="request">The seek request.</param> + /// <param name="context">The context of the state.</param> + /// <param name="prevState">The previous state.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void HandleRequest(SeekGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Handles a buffer request from a session. Context's state can change. + /// </summary> + /// <param name="request">The buffer request.</param> + /// <param name="context">The context of the state.</param> + /// <param name="prevState">The previous state.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void HandleRequest(BufferGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Handles a ready request from a session. Context's state can change. + /// </summary> + /// <param name="request">The ready request.</param> + /// <param name="context">The context of the state.</param> + /// <param name="prevState">The previous state.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void HandleRequest(ReadyGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Handles a next-item request from a session. Context's state can change. + /// </summary> + /// <param name="request">The next-item request.</param> + /// <param name="context">The context of the state.</param> + /// <param name="prevState">The previous state.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void HandleRequest(NextItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Handles a previous-item request from a session. Context's state can change. + /// </summary> + /// <param name="request">The previous-item request.</param> + /// <param name="context">The context of the state.</param> + /// <param name="prevState">The previous state.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void HandleRequest(PreviousItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Handles a set-repeat-mode request from a session. Context's state should not change. + /// </summary> + /// <param name="request">The repeat-mode request.</param> + /// <param name="context">The context of the state.</param> + /// <param name="prevState">The previous state.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void HandleRequest(SetRepeatModeGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Handles a set-shuffle-mode request from a session. Context's state should not change. + /// </summary> + /// <param name="request">The shuffle-mode request.</param> + /// <param name="context">The context of the state.</param> + /// <param name="prevState">The previous state.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void HandleRequest(SetShuffleModeGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Updates the ping of a session. Context's state should not change. + /// </summary> + /// <param name="request">The ping request.</param> + /// <param name="context">The context of the state.</param> + /// <param name="prevState">The previous state.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void HandleRequest(PingGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Handles a ignore-wait request from a session. Context's state can change. + /// </summary> + /// <param name="request">The ignore-wait request.</param> + /// <param name="context">The context of the state.</param> + /// <param name="prevState">The previous state.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void HandleRequest(IgnoreWaitGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs b/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs new file mode 100644 index 000000000..d2de22450 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs @@ -0,0 +1,230 @@ +#nullable disable + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.SyncPlay.Queue; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// <summary> + /// Interface IGroupStateContext. + /// </summary> + public interface IGroupStateContext + { + /// <summary> + /// Gets the default ping value used for sessions, in milliseconds. + /// </summary> + /// <value>The default ping value used for sessions, in milliseconds.</value> + long DefaultPing { get; } + + /// <summary> + /// Gets the maximum time offset error accepted for dates reported by clients, in milliseconds. + /// </summary> + /// <value>The maximum offset error accepted, in milliseconds.</value> + long TimeSyncOffset { get; } + + /// <summary> + /// Gets the maximum offset error accepted for position reported by clients, in milliseconds. + /// </summary> + /// <value>The maximum offset error accepted, in milliseconds.</value> + long MaxPlaybackOffset { get; } + + /// <summary> + /// Gets the group identifier. + /// </summary> + /// <value>The group identifier.</value> + Guid GroupId { get; } + + /// <summary> + /// Gets or sets the position ticks. + /// </summary> + /// <value>The position ticks.</value> + long PositionTicks { get; set; } + + /// <summary> + /// Gets or sets the last activity. + /// </summary> + /// <value>The last activity.</value> + DateTime LastActivity { get; set; } + + /// <summary> + /// Gets the play queue. + /// </summary> + /// <value>The play queue.</value> + PlayQueueManager PlayQueue { get; } + + /// <summary> + /// Sets a new state. + /// </summary> + /// <param name="state">The new state.</param> + void SetState(IGroupState state); + + /// <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> + /// <returns>The task.</returns> + Task SendGroupUpdate<T>(SessionInfo from, SyncPlayBroadcastType type, GroupUpdate<T> message, CancellationToken cancellationToken); + + /// <summary> + /// Sends a playback command to the interested sessions. + /// </summary> + /// <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> + /// <returns>The task.</returns> + Task SendCommand(SessionInfo from, SyncPlayBroadcastType type, SendCommand message, CancellationToken cancellationToken); + + /// <summary> + /// Builds a new playback command with some default values. + /// </summary> + /// <param name="type">The command type.</param> + /// <returns>The command.</returns> + 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> + /// <returns>The sanitized position ticks.</returns> + long SanitizePositionTicks(long? positionTicks); + + /// <summary> + /// Updates the ping of a session, in milliseconds. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="ping">The ping, in milliseconds.</param> + void UpdatePing(SessionInfo session, long ping); + + /// <summary> + /// Gets the highest ping in the group, in milliseconds. + /// </summary> + /// <returns>The highest ping in the group.</returns> + long GetHighestPing(); + + /// <summary> + /// Sets the session's buffering state. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="isBuffering">The state.</param> + void SetBuffering(SessionInfo session, bool isBuffering); + + /// <summary> + /// Sets the buffering state of all the sessions. + /// </summary> + /// <param name="isBuffering">The state.</param> + void SetAllBuffering(bool isBuffering); + + /// <summary> + /// Gets the group buffering state. + /// </summary> + /// <returns><c>true</c> if there is a session buffering in the group; <c>false</c> otherwise.</returns> + bool IsBuffering(); + + /// <summary> + /// Sets the session's group wait state. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="ignoreGroupWait">The state.</param> + void SetIgnoreGroupWait(SessionInfo session, bool ignoreGroupWait); + + /// <summary> + /// Sets a new play queue. + /// </summary> + /// <param name="playQueue">The new play queue.</param> + /// <param name="playingItemPosition">The playing item position in the play queue.</param> + /// <param name="startPositionTicks">The start position ticks.</param> + /// <returns><c>true</c> if the play queue has been changed; <c>false</c> if something went wrong.</returns> + bool SetPlayQueue(IReadOnlyList<Guid> playQueue, int playingItemPosition, long startPositionTicks); + + /// <summary> + /// Sets the playing item. + /// </summary> + /// <param name="playlistItemId">The new playing item identifier.</param> + /// <returns><c>true</c> if the play queue has been changed; <c>false</c> if something went wrong.</returns> + bool SetPlayingItem(Guid playlistItemId); + + /// <summary> + /// Clears the play queue. + /// </summary> + /// <param name="clearPlayingItem">Whether to remove the playing item as well.</param> + void ClearPlayQueue(bool clearPlayingItem); + + /// <summary> + /// Removes items from the play queue. + /// </summary> + /// <param name="playlistItemIds">The items to remove.</param> + /// <returns><c>true</c> if playing item got removed; <c>false</c> otherwise.</returns> + bool RemoveFromPlayQueue(IReadOnlyList<Guid> playlistItemIds); + + /// <summary> + /// Moves an item in the play queue. + /// </summary> + /// <param name="playlistItemId">The playlist identifier of the item to move.</param> + /// <param name="newIndex">The new position.</param> + /// <returns><c>true</c> if item has been moved; <c>false</c> if something went wrong.</returns> + bool MoveItemInPlayQueue(Guid playlistItemId, int newIndex); + + /// <summary> + /// Updates the play queue. + /// </summary> + /// <param name="newItems">The new items to add to the play queue.</param> + /// <param name="mode">The mode with which the items will be added.</param> + /// <returns><c>true</c> if the play queue has been changed; <c>false</c> if something went wrong.</returns> + bool AddToPlayQueue(IReadOnlyList<Guid> newItems, GroupQueueMode mode); + + /// <summary> + /// Restarts current item in play queue. + /// </summary> + void RestartCurrentItem(); + + /// <summary> + /// Picks next item in play queue. + /// </summary> + /// <returns><c>true</c> if the item changed; <c>false</c> otherwise.</returns> + bool NextItemInQueue(); + + /// <summary> + /// Picks previous item in play queue. + /// </summary> + /// <returns><c>true</c> if the item changed; <c>false</c> otherwise.</returns> + bool PreviousItemInQueue(); + + /// <summary> + /// Sets the repeat mode. + /// </summary> + /// <param name="mode">The new mode.</param> + void SetRepeatMode(GroupRepeatMode mode); + + /// <summary> + /// Sets the shuffle mode. + /// </summary> + /// <param name="mode">The new mode.</param> + void SetShuffleMode(GroupShuffleMode mode); + + /// <summary> + /// Creates a play queue update. + /// </summary> + /// <param name="reason">The reason for the update.</param> + /// <returns>The play queue update.</returns> + PlayQueueUpdate GetPlayQueueUpdate(PlayQueueUpdateReason reason); + } +} diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs new file mode 100644 index 000000000..a6999a12c --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs @@ -0,0 +1,64 @@ +#nullable disable + +using System; +using System.Collections.Generic; +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.SyncPlay.Requests; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// <summary> + /// Interface ISyncPlayManager. + /// </summary> + public interface ISyncPlayManager + { + /// <summary> + /// Creates a new group. + /// </summary> + /// <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); + + /// <summary> + /// Adds the session to a group. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="request">The request.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void JoinGroup(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken); + + /// <summary> + /// Removes the session from a group. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="request">The request.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void LeaveGroup(SessionInfo session, LeaveGroupRequest request, CancellationToken cancellationToken); + + /// <summary> + /// Gets list of available groups for a session. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="request">The request.</param> + /// <returns>The list of available groups.</returns> + List<GroupInfoDto> ListGroups(SessionInfo session, ListGroupsRequest request); + + /// <summary> + /// Handle a request by a session in a group. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="request">The request.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void HandleRequest(SessionInfo session, IGroupPlaybackRequest request, CancellationToken cancellationToken); + + /// <summary> + /// Checks whether a user has an active session using SyncPlay. + /// </summary> + /// <param name="userId">The user identifier to check.</param> + /// <returns><c>true</c> if the user is using SyncPlay; <c>false</c> otherwise.</returns> + bool IsUserActive(Guid userId); + } +} diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayRequest.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayRequest.cs new file mode 100644 index 000000000..bf1981773 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayRequest.cs @@ -0,0 +1,16 @@ +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// <summary> + /// Interface ISyncPlayRequest. + /// </summary> + public interface ISyncPlayRequest + { + /// <summary> + /// Gets the request type. + /// </summary> + /// <returns>The request type.</returns> + RequestType Type { get; } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/AbstractPlaybackRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/AbstractPlaybackRequest.cs new file mode 100644 index 000000000..ef496c103 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/AbstractPlaybackRequest.cs @@ -0,0 +1,31 @@ +#nullable disable + +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests +{ + /// <summary> + /// Class AbstractPlaybackRequest. + /// </summary> + public abstract class AbstractPlaybackRequest : IGroupPlaybackRequest + { + /// <summary> + /// Initializes a new instance of the <see cref="AbstractPlaybackRequest"/> class. + /// </summary> + protected AbstractPlaybackRequest() + { + // Do nothing. + } + + /// <inheritdoc /> + public RequestType Type { get; } = RequestType.Playback; + + /// <inheritdoc /> + public abstract PlaybackRequestType Action { get; } + + /// <inheritdoc /> + public abstract void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/BufferGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/BufferGroupRequest.cs new file mode 100644 index 000000000..d188114c3 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/BufferGroupRequest.cs @@ -0,0 +1,63 @@ +#nullable disable + +using System; +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests +{ + /// <summary> + /// Class BufferGroupRequest. + /// </summary> + public class BufferGroupRequest : AbstractPlaybackRequest + { + /// <summary> + /// Initializes a new instance of the <see cref="BufferGroupRequest"/> class. + /// </summary> + /// <param name="when">When the request has been made, as reported by the client.</param> + /// <param name="positionTicks">The position ticks.</param> + /// <param name="isPlaying">Whether the client playback is unpaused.</param> + /// <param name="playlistItemId">The playlist item identifier of the playing item.</param> + public BufferGroupRequest(DateTime when, long positionTicks, bool isPlaying, Guid playlistItemId) + { + When = when; + PositionTicks = positionTicks; + IsPlaying = isPlaying; + PlaylistItemId = playlistItemId; + } + + /// <summary> + /// Gets when the request has been made by the client. + /// </summary> + /// <value>The date of the request.</value> + public DateTime When { get; } + + /// <summary> + /// Gets the position ticks. + /// </summary> + /// <value>The position ticks.</value> + public long PositionTicks { get; } + + /// <summary> + /// Gets a value indicating whether the client playback is unpaused. + /// </summary> + /// <value>The client playback status.</value> + public bool IsPlaying { get; } + + /// <summary> + /// Gets the playlist item identifier of the playing item. + /// </summary> + /// <value>The playlist item identifier.</value> + public Guid PlaylistItemId { get; } + + /// <inheritdoc /> + public override PlaybackRequestType Action { get; } = PlaybackRequestType.Buffer; + + /// <inheritdoc /> + public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken) + { + state.HandleRequest(this, context, state.Type, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/IgnoreWaitGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/IgnoreWaitGroupRequest.cs new file mode 100644 index 000000000..464c81dfd --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/IgnoreWaitGroupRequest.cs @@ -0,0 +1,38 @@ +#nullable disable + +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests +{ + /// <summary> + /// Class IgnoreWaitGroupRequest. + /// </summary> + public class IgnoreWaitGroupRequest : AbstractPlaybackRequest + { + /// <summary> + /// Initializes a new instance of the <see cref="IgnoreWaitGroupRequest"/> class. + /// </summary> + /// <param name="ignoreWait">Whether the client should be ignored.</param> + public IgnoreWaitGroupRequest(bool ignoreWait) + { + IgnoreWait = ignoreWait; + } + + /// <summary> + /// Gets a value indicating whether the client should be ignored. + /// </summary> + /// <value>The client group-wait status.</value> + public bool IgnoreWait { get; } + + /// <inheritdoc /> + public override PlaybackRequestType Action { get; } = PlaybackRequestType.IgnoreWait; + + /// <inheritdoc /> + public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken) + { + state.HandleRequest(this, context, state.Type, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/MovePlaylistItemGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/MovePlaylistItemGroupRequest.cs new file mode 100644 index 000000000..be314e807 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/MovePlaylistItemGroupRequest.cs @@ -0,0 +1,47 @@ +#nullable disable + +using System; +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests +{ + /// <summary> + /// Class MovePlaylistItemGroupRequest. + /// </summary> + public class MovePlaylistItemGroupRequest : AbstractPlaybackRequest + { + /// <summary> + /// Initializes a new instance of the <see cref="MovePlaylistItemGroupRequest"/> class. + /// </summary> + /// <param name="playlistItemId">The playlist identifier of the item.</param> + /// <param name="newIndex">The new position.</param> + public MovePlaylistItemGroupRequest(Guid playlistItemId, int newIndex) + { + PlaylistItemId = playlistItemId; + NewIndex = newIndex; + } + + /// <summary> + /// Gets the playlist identifier of the item. + /// </summary> + /// <value>The playlist identifier of the item.</value> + public Guid PlaylistItemId { get; } + + /// <summary> + /// Gets the new position. + /// </summary> + /// <value>The new position.</value> + public int NewIndex { get; } + + /// <inheritdoc /> + public override PlaybackRequestType Action { get; } = PlaybackRequestType.MovePlaylistItem; + + /// <inheritdoc /> + public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken) + { + state.HandleRequest(this, context, state.Type, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/NextItemGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/NextItemGroupRequest.cs new file mode 100644 index 000000000..679076239 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/NextItemGroupRequest.cs @@ -0,0 +1,39 @@ +#nullable disable + +using System; +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests +{ + /// <summary> + /// Class NextItemGroupRequest. + /// </summary> + public class NextItemGroupRequest : AbstractPlaybackRequest + { + /// <summary> + /// Initializes a new instance of the <see cref="NextItemGroupRequest"/> class. + /// </summary> + /// <param name="playlistItemId">The playing item identifier.</param> + public NextItemGroupRequest(Guid playlistItemId) + { + PlaylistItemId = playlistItemId; + } + + /// <summary> + /// Gets the playing item identifier. + /// </summary> + /// <value>The playing item identifier.</value> + public Guid PlaylistItemId { get; } + + /// <inheritdoc /> + public override PlaybackRequestType Action { get; } = PlaybackRequestType.NextItem; + + /// <inheritdoc /> + public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken) + { + state.HandleRequest(this, context, state.Type, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PauseGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PauseGroupRequest.cs new file mode 100644 index 000000000..7ee18a366 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PauseGroupRequest.cs @@ -0,0 +1,23 @@ +#nullable disable + +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests +{ + /// <summary> + /// Class PauseGroupRequest. + /// </summary> + public class PauseGroupRequest : AbstractPlaybackRequest + { + /// <inheritdoc /> + public override PlaybackRequestType Action { get; } = PlaybackRequestType.Pause; + + /// <inheritdoc /> + public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken) + { + state.HandleRequest(this, context, state.Type, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PingGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PingGroupRequest.cs new file mode 100644 index 000000000..beab655c5 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PingGroupRequest.cs @@ -0,0 +1,38 @@ +#nullable disable + +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests +{ + /// <summary> + /// Class PingGroupRequest. + /// </summary> + public class PingGroupRequest : AbstractPlaybackRequest + { + /// <summary> + /// Initializes a new instance of the <see cref="PingGroupRequest"/> class. + /// </summary> + /// <param name="ping">The ping time.</param> + public PingGroupRequest(long ping) + { + Ping = ping; + } + + /// <summary> + /// Gets the ping time. + /// </summary> + /// <value>The ping time.</value> + public long Ping { get; } + + /// <inheritdoc /> + public override PlaybackRequestType Action { get; } = PlaybackRequestType.Ping; + + /// <inheritdoc /> + public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken) + { + state.HandleRequest(this, context, state.Type, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PlayGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PlayGroupRequest.cs new file mode 100644 index 000000000..05ff262c1 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PlayGroupRequest.cs @@ -0,0 +1,56 @@ +#nullable disable + +using System; +using System.Collections.Generic; +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests +{ + /// <summary> + /// Class PlayGroupRequest. + /// </summary> + public class PlayGroupRequest : AbstractPlaybackRequest + { + /// <summary> + /// Initializes a new instance of the <see cref="PlayGroupRequest"/> class. + /// </summary> + /// <param name="playingQueue">The playing queue.</param> + /// <param name="playingItemPosition">The playing item position.</param> + /// <param name="startPositionTicks">The start position ticks.</param> + public PlayGroupRequest(IReadOnlyList<Guid> playingQueue, int playingItemPosition, long startPositionTicks) + { + PlayingQueue = playingQueue ?? Array.Empty<Guid>(); + PlayingItemPosition = playingItemPosition; + StartPositionTicks = startPositionTicks; + } + + /// <summary> + /// Gets the playing queue. + /// </summary> + /// <value>The playing queue.</value> + public IReadOnlyList<Guid> PlayingQueue { get; } + + /// <summary> + /// Gets the position of the playing item in the queue. + /// </summary> + /// <value>The playing item position.</value> + public int PlayingItemPosition { get; } + + /// <summary> + /// Gets the start position ticks. + /// </summary> + /// <value>The start position ticks.</value> + public long StartPositionTicks { get; } + + /// <inheritdoc /> + public override PlaybackRequestType Action { get; } = PlaybackRequestType.Play; + + /// <inheritdoc /> + public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken) + { + state.HandleRequest(this, context, state.Type, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PreviousItemGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PreviousItemGroupRequest.cs new file mode 100644 index 000000000..3e34b6ce4 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PreviousItemGroupRequest.cs @@ -0,0 +1,39 @@ +#nullable disable + +using System; +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests +{ + /// <summary> + /// Class PreviousItemGroupRequest. + /// </summary> + public class PreviousItemGroupRequest : AbstractPlaybackRequest + { + /// <summary> + /// Initializes a new instance of the <see cref="PreviousItemGroupRequest"/> class. + /// </summary> + /// <param name="playlistItemId">The playing item identifier.</param> + public PreviousItemGroupRequest(Guid playlistItemId) + { + PlaylistItemId = playlistItemId; + } + + /// <summary> + /// Gets the playing item identifier. + /// </summary> + /// <value>The playing item identifier.</value> + public Guid PlaylistItemId { get; } + + /// <inheritdoc /> + public override PlaybackRequestType Action { get; } = PlaybackRequestType.PreviousItem; + + /// <inheritdoc /> + public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken) + { + state.HandleRequest(this, context, state.Type, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/QueueGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/QueueGroupRequest.cs new file mode 100644 index 000000000..0f91476de --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/QueueGroupRequest.cs @@ -0,0 +1,48 @@ +#nullable disable + +using System; +using System.Collections.Generic; +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests +{ + /// <summary> + /// Class QueueGroupRequest. + /// </summary> + public class QueueGroupRequest : AbstractPlaybackRequest + { + /// <summary> + /// Initializes a new instance of the <see cref="QueueGroupRequest"/> class. + /// </summary> + /// <param name="items">The items to add to the queue.</param> + /// <param name="mode">The enqueue mode.</param> + public QueueGroupRequest(IReadOnlyList<Guid> items, GroupQueueMode mode) + { + ItemIds = items ?? Array.Empty<Guid>(); + Mode = mode; + } + + /// <summary> + /// Gets the items to enqueue. + /// </summary> + /// <value>The items to enqueue.</value> + public IReadOnlyList<Guid> ItemIds { get; } + + /// <summary> + /// Gets the mode in which to add the new items. + /// </summary> + /// <value>The enqueue mode.</value> + public GroupQueueMode Mode { get; } + + /// <inheritdoc /> + public override PlaybackRequestType Action { get; } = PlaybackRequestType.Queue; + + /// <inheritdoc /> + public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken) + { + state.HandleRequest(this, context, state.Type, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/ReadyGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/ReadyGroupRequest.cs new file mode 100644 index 000000000..b1f0bd360 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/ReadyGroupRequest.cs @@ -0,0 +1,63 @@ +#nullable disable + +using System; +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests +{ + /// <summary> + /// Class ReadyGroupRequest. + /// </summary> + public class ReadyGroupRequest : AbstractPlaybackRequest + { + /// <summary> + /// Initializes a new instance of the <see cref="ReadyGroupRequest"/> class. + /// </summary> + /// <param name="when">When the request has been made, as reported by the client.</param> + /// <param name="positionTicks">The position ticks.</param> + /// <param name="isPlaying">Whether the client playback is unpaused.</param> + /// <param name="playlistItemId">The playlist item identifier of the playing item.</param> + public ReadyGroupRequest(DateTime when, long positionTicks, bool isPlaying, Guid playlistItemId) + { + When = when; + PositionTicks = positionTicks; + IsPlaying = isPlaying; + PlaylistItemId = playlistItemId; + } + + /// <summary> + /// Gets when the request has been made by the client. + /// </summary> + /// <value>The date of the request.</value> + public DateTime When { get; } + + /// <summary> + /// Gets the position ticks. + /// </summary> + /// <value>The position ticks.</value> + public long PositionTicks { get; } + + /// <summary> + /// Gets a value indicating whether the client playback is unpaused. + /// </summary> + /// <value>The client playback status.</value> + public bool IsPlaying { get; } + + /// <summary> + /// Gets the playlist item identifier of the playing item. + /// </summary> + /// <value>The playlist item identifier.</value> + public Guid PlaylistItemId { get; } + + /// <inheritdoc /> + public override PlaybackRequestType Action { get; } = PlaybackRequestType.Ready; + + /// <inheritdoc /> + public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken) + { + state.HandleRequest(this, context, state.Type, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs new file mode 100644 index 000000000..2f38d6adc --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs @@ -0,0 +1,56 @@ +#nullable disable + +using System; +using System.Collections.Generic; +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests +{ + /// <summary> + /// Class RemoveFromPlaylistGroupRequest. + /// </summary> + public class RemoveFromPlaylistGroupRequest : AbstractPlaybackRequest + { + /// <summary> + /// Initializes a new instance of the <see cref="RemoveFromPlaylistGroupRequest"/> class. + /// </summary> + /// <param name="items">The playlist ids of the items to remove.</param> + /// <param name="clearPlaylist">Whether to clear the entire playlist. The items list will be ignored.</param> + /// <param name="clearPlayingItem">Whether to remove the playing item as well. Used only when clearing the playlist.</param> + public RemoveFromPlaylistGroupRequest(IReadOnlyList<Guid> items, bool clearPlaylist = false, bool clearPlayingItem = false) + { + PlaylistItemIds = items ?? Array.Empty<Guid>(); + ClearPlaylist = clearPlaylist; + ClearPlayingItem = clearPlayingItem; + } + + /// <summary> + /// Gets the playlist identifiers ot the items. + /// </summary> + /// <value>The playlist identifiers ot the items.</value> + public IReadOnlyList<Guid> PlaylistItemIds { get; } + + /// <summary> + /// Gets a value indicating whether the entire playlist should be cleared. + /// </summary> + /// <value>Whether the entire playlist should be cleared.</value> + public bool ClearPlaylist { get; } + + /// <summary> + /// Gets a value indicating whether the playing item should be removed as well. + /// </summary> + /// <value>Whether the playing item should be removed as well.</value> + public bool ClearPlayingItem { get; } + + /// <inheritdoc /> + public override PlaybackRequestType Action { get; } = PlaybackRequestType.RemoveFromPlaylist; + + /// <inheritdoc /> + public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken) + { + state.HandleRequest(this, context, state.Type, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SeekGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SeekGroupRequest.cs new file mode 100644 index 000000000..196113374 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SeekGroupRequest.cs @@ -0,0 +1,38 @@ +#nullable disable + +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests +{ + /// <summary> + /// Class SeekGroupRequest. + /// </summary> + public class SeekGroupRequest : AbstractPlaybackRequest + { + /// <summary> + /// Initializes a new instance of the <see cref="SeekGroupRequest"/> class. + /// </summary> + /// <param name="positionTicks">The position ticks.</param> + public SeekGroupRequest(long positionTicks) + { + PositionTicks = positionTicks; + } + + /// <summary> + /// Gets the position ticks. + /// </summary> + /// <value>The position ticks.</value> + public long PositionTicks { get; } + + /// <inheritdoc /> + public override PlaybackRequestType Action { get; } = PlaybackRequestType.Seek; + + /// <inheritdoc /> + public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken) + { + state.HandleRequest(this, context, state.Type, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetPlaylistItemGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetPlaylistItemGroupRequest.cs new file mode 100644 index 000000000..44df127a6 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetPlaylistItemGroupRequest.cs @@ -0,0 +1,39 @@ +#nullable disable + +using System; +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests +{ + /// <summary> + /// Class SetPlaylistItemGroupRequest. + /// </summary> + public class SetPlaylistItemGroupRequest : AbstractPlaybackRequest + { + /// <summary> + /// Initializes a new instance of the <see cref="SetPlaylistItemGroupRequest"/> class. + /// </summary> + /// <param name="playlistItemId">The playlist identifier of the item.</param> + public SetPlaylistItemGroupRequest(Guid playlistItemId) + { + PlaylistItemId = playlistItemId; + } + + /// <summary> + /// Gets the playlist identifier of the playing item. + /// </summary> + /// <value>The playlist identifier of the playing item.</value> + public Guid PlaylistItemId { get; } + + /// <inheritdoc /> + public override PlaybackRequestType Action { get; } = PlaybackRequestType.SetPlaylistItem; + + /// <inheritdoc /> + public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken) + { + state.HandleRequest(this, context, state.Type, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetRepeatModeGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetRepeatModeGroupRequest.cs new file mode 100644 index 000000000..d250eab56 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetRepeatModeGroupRequest.cs @@ -0,0 +1,38 @@ +#nullable disable + +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests +{ + /// <summary> + /// Class SetRepeatModeGroupRequest. + /// </summary> + public class SetRepeatModeGroupRequest : AbstractPlaybackRequest + { + /// <summary> + /// Initializes a new instance of the <see cref="SetRepeatModeGroupRequest"/> class. + /// </summary> + /// <param name="mode">The repeat mode.</param> + public SetRepeatModeGroupRequest(GroupRepeatMode mode) + { + Mode = mode; + } + + /// <summary> + /// Gets the repeat mode. + /// </summary> + /// <value>The repeat mode.</value> + public GroupRepeatMode Mode { get; } + + /// <inheritdoc /> + public override PlaybackRequestType Action { get; } = PlaybackRequestType.SetRepeatMode; + + /// <inheritdoc /> + public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken) + { + state.HandleRequest(this, context, state.Type, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetShuffleModeGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetShuffleModeGroupRequest.cs new file mode 100644 index 000000000..5034e992e --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetShuffleModeGroupRequest.cs @@ -0,0 +1,38 @@ +#nullable disable + +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests +{ + /// <summary> + /// Class SetShuffleModeGroupRequest. + /// </summary> + public class SetShuffleModeGroupRequest : AbstractPlaybackRequest + { + /// <summary> + /// Initializes a new instance of the <see cref="SetShuffleModeGroupRequest"/> class. + /// </summary> + /// <param name="mode">The shuffle mode.</param> + public SetShuffleModeGroupRequest(GroupShuffleMode mode) + { + Mode = mode; + } + + /// <summary> + /// Gets the shuffle mode. + /// </summary> + /// <value>The shuffle mode.</value> + public GroupShuffleMode Mode { get; } + + /// <inheritdoc /> + public override PlaybackRequestType Action { get; } = PlaybackRequestType.SetShuffleMode; + + /// <inheritdoc /> + public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken) + { + state.HandleRequest(this, context, state.Type, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/StopGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/StopGroupRequest.cs new file mode 100644 index 000000000..ad739213c --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/StopGroupRequest.cs @@ -0,0 +1,21 @@ +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests +{ + /// <summary> + /// Class StopGroupRequest. + /// </summary> + public class StopGroupRequest : AbstractPlaybackRequest + { + /// <inheritdoc /> + public override PlaybackRequestType Action { get; } = PlaybackRequestType.Stop; + + /// <inheritdoc /> + public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken) + { + state.HandleRequest(this, context, state.Type, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/UnpauseGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/UnpauseGroupRequest.cs new file mode 100644 index 000000000..aaf3d65a8 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/UnpauseGroupRequest.cs @@ -0,0 +1,21 @@ +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests +{ + /// <summary> + /// Class UnpauseGroupRequest. + /// </summary> + public class UnpauseGroupRequest : AbstractPlaybackRequest + { + /// <inheritdoc /> + public override PlaybackRequestType Action { get; } = PlaybackRequestType.Unpause; + + /// <inheritdoc /> + public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken) + { + state.HandleRequest(this, context, state.Type, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs new file mode 100644 index 000000000..f49876cca --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs @@ -0,0 +1,557 @@ +#nullable disable + +using System; +using System.Collections.Generic; +using System.Linq; +using Jellyfin.Extensions; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay.Queue +{ + /// <summary> + /// Class PlayQueueManager. + /// </summary> + public class PlayQueueManager + { + /// <summary> + /// Placeholder index for when no item is playing. + /// </summary> + /// <value>The no-playing item index.</value> + private const int NoPlayingItemIndex = -1; + + /// <summary> + /// The sorted playlist. + /// </summary> + /// <value>The sorted playlist, or play queue of the group.</value> + private List<QueueItem> _sortedPlaylist = new List<QueueItem>(); + + /// <summary> + /// The shuffled playlist. + /// </summary> + /// <value>The shuffled playlist, or play queue of the group.</value> + private List<QueueItem> _shuffledPlaylist = new List<QueueItem>(); + + /// <summary> + /// Initializes a new instance of the <see cref="PlayQueueManager" /> class. + /// </summary> + public PlayQueueManager() + { + Reset(); + } + + /// <summary> + /// Gets the playing item index. + /// </summary> + /// <value>The playing item index.</value> + public int PlayingItemIndex { get; private set; } + + /// <summary> + /// Gets the last time the queue has been changed. + /// </summary> + /// <value>The last time the queue has been changed.</value> + public DateTime LastChange { get; private set; } + + /// <summary> + /// Gets the shuffle mode. + /// </summary> + /// <value>The shuffle mode.</value> + public GroupShuffleMode ShuffleMode { get; private set; } = GroupShuffleMode.Sorted; + + /// <summary> + /// Gets the repeat mode. + /// </summary> + /// <value>The repeat mode.</value> + public GroupRepeatMode RepeatMode { get; private set; } = GroupRepeatMode.RepeatNone; + + /// <summary> + /// Checks if an item is playing. + /// </summary> + /// <returns><c>true</c> if an item is playing; <c>false</c> otherwise.</returns> + public bool IsItemPlaying() + { + return PlayingItemIndex != NoPlayingItemIndex; + } + + /// <summary> + /// Gets the current playlist considering the shuffle mode. + /// </summary> + /// <returns>The playlist.</returns> + public IReadOnlyList<QueueItem> GetPlaylist() + { + return GetPlaylistInternal(); + } + + /// <summary> + /// Sets a new playlist. Playing item is reset. + /// </summary> + /// <param name="items">The new items of the playlist.</param> + public void SetPlaylist(IReadOnlyList<Guid> items) + { + _sortedPlaylist.Clear(); + _shuffledPlaylist.Clear(); + + _sortedPlaylist = CreateQueueItemsFromArray(items); + if (ShuffleMode.Equals(GroupShuffleMode.Shuffle)) + { + _shuffledPlaylist = new List<QueueItem>(_sortedPlaylist); + _shuffledPlaylist.Shuffle(); + } + + PlayingItemIndex = NoPlayingItemIndex; + LastChange = DateTime.UtcNow; + } + + /// <summary> + /// Appends new items to the playlist. The specified order is mantained. + /// </summary> + /// <param name="items">The items to add to the playlist.</param> + public void Queue(IReadOnlyList<Guid> items) + { + var newItems = CreateQueueItemsFromArray(items); + + _sortedPlaylist.AddRange(newItems); + if (ShuffleMode.Equals(GroupShuffleMode.Shuffle)) + { + _shuffledPlaylist.AddRange(newItems); + } + + LastChange = DateTime.UtcNow; + } + + /// <summary> + /// Shuffles the playlist. Shuffle mode is changed. The playlist gets re-shuffled if already shuffled. + /// </summary> + public void ShufflePlaylist() + { + if (PlayingItemIndex == NoPlayingItemIndex) + { + _shuffledPlaylist = new List<QueueItem>(_sortedPlaylist); + _shuffledPlaylist.Shuffle(); + } + else if (ShuffleMode.Equals(GroupShuffleMode.Sorted)) + { + // First time shuffle. + var playingItem = _sortedPlaylist[PlayingItemIndex]; + _shuffledPlaylist = new List<QueueItem>(_sortedPlaylist); + _shuffledPlaylist.RemoveAt(PlayingItemIndex); + _shuffledPlaylist.Shuffle(); + _shuffledPlaylist.Insert(0, playingItem); + PlayingItemIndex = 0; + } + else + { + // Re-shuffle playlist. + var playingItem = _shuffledPlaylist[PlayingItemIndex]; + _shuffledPlaylist.RemoveAt(PlayingItemIndex); + _shuffledPlaylist.Shuffle(); + _shuffledPlaylist.Insert(0, playingItem); + PlayingItemIndex = 0; + } + + ShuffleMode = GroupShuffleMode.Shuffle; + LastChange = DateTime.UtcNow; + } + + /// <summary> + /// Resets the playlist to sorted mode. Shuffle mode is changed. + /// </summary> + public void RestoreSortedPlaylist() + { + if (PlayingItemIndex != NoPlayingItemIndex) + { + var playingItem = _shuffledPlaylist[PlayingItemIndex]; + PlayingItemIndex = _sortedPlaylist.IndexOf(playingItem); + } + + _shuffledPlaylist.Clear(); + + ShuffleMode = GroupShuffleMode.Sorted; + LastChange = DateTime.UtcNow; + } + + /// <summary> + /// Clears the playlist. Shuffle mode is preserved. + /// </summary> + /// <param name="clearPlayingItem">Whether to remove the playing item as well.</param> + public void ClearPlaylist(bool clearPlayingItem) + { + var playingItem = GetPlayingItem(); + _sortedPlaylist.Clear(); + _shuffledPlaylist.Clear(); + LastChange = DateTime.UtcNow; + + if (!clearPlayingItem && playingItem != null) + { + _sortedPlaylist.Add(playingItem); + if (ShuffleMode.Equals(GroupShuffleMode.Shuffle)) + { + _shuffledPlaylist.Add(playingItem); + } + + PlayingItemIndex = 0; + } + else + { + PlayingItemIndex = NoPlayingItemIndex; + } + } + + /// <summary> + /// Adds new items to the playlist right after the playing item. The specified order is mantained. + /// </summary> + /// <param name="items">The items to add to the playlist.</param> + public void QueueNext(IReadOnlyList<Guid> items) + { + var newItems = CreateQueueItemsFromArray(items); + + if (ShuffleMode.Equals(GroupShuffleMode.Shuffle)) + { + var playingItem = GetPlayingItem(); + var sortedPlayingItemIndex = _sortedPlaylist.IndexOf(playingItem); + // Append items to sorted and shuffled playlist as they are. + _sortedPlaylist.InsertRange(sortedPlayingItemIndex + 1, newItems); + _shuffledPlaylist.InsertRange(PlayingItemIndex + 1, newItems); + } + else + { + _sortedPlaylist.InsertRange(PlayingItemIndex + 1, newItems); + } + + LastChange = DateTime.UtcNow; + } + + /// <summary> + /// Gets playlist identifier of the playing item, if any. + /// </summary> + /// <returns>The playlist identifier of the playing item.</returns> + public Guid GetPlayingItemPlaylistId() + { + var playingItem = GetPlayingItem(); + return playingItem?.PlaylistItemId ?? Guid.Empty; + } + + /// <summary> + /// Gets the playing item identifier, if any. + /// </summary> + /// <returns>The playing item identifier.</returns> + public Guid GetPlayingItemId() + { + var playingItem = GetPlayingItem(); + return playingItem?.ItemId ?? Guid.Empty; + } + + /// <summary> + /// Sets the playing item using its identifier. If not in the playlist, the playing item is reset. + /// </summary> + /// <param name="itemId">The new playing item identifier.</param> + public void SetPlayingItemById(Guid itemId) + { + var playlist = GetPlaylistInternal(); + PlayingItemIndex = playlist.FindIndex(item => item.ItemId.Equals(itemId)); + LastChange = DateTime.UtcNow; + } + + /// <summary> + /// Sets the playing item using its playlist identifier. If not in the playlist, the playing item is reset. + /// </summary> + /// <param name="playlistItemId">The new playing item identifier.</param> + /// <returns><c>true</c> if playing item has been set; <c>false</c> if item is not in the playlist.</returns> + public bool SetPlayingItemByPlaylistId(Guid playlistItemId) + { + var playlist = GetPlaylistInternal(); + PlayingItemIndex = playlist.FindIndex(item => item.PlaylistItemId.Equals(playlistItemId)); + LastChange = DateTime.UtcNow; + + return PlayingItemIndex != NoPlayingItemIndex; + } + + /// <summary> + /// Sets the playing item using its position. If not in range, the playing item is reset. + /// </summary> + /// <param name="playlistIndex">The new playing item index.</param> + public void SetPlayingItemByIndex(int playlistIndex) + { + var playlist = GetPlaylistInternal(); + if (playlistIndex < 0 || playlistIndex > playlist.Count) + { + PlayingItemIndex = NoPlayingItemIndex; + } + else + { + PlayingItemIndex = playlistIndex; + } + + LastChange = DateTime.UtcNow; + } + + /// <summary> + /// Removes items from the playlist. If not removed, the playing item is preserved. + /// </summary> + /// <param name="playlistItemIds">The items to remove.</param> + /// <returns><c>true</c> if playing item got removed; <c>false</c> otherwise.</returns> + public bool RemoveFromPlaylist(IReadOnlyList<Guid> playlistItemIds) + { + var playingItem = GetPlayingItem(); + + _sortedPlaylist.RemoveAll(item => playlistItemIds.Contains(item.PlaylistItemId)); + _shuffledPlaylist.RemoveAll(item => playlistItemIds.Contains(item.PlaylistItemId)); + + LastChange = DateTime.UtcNow; + + if (playingItem != null) + { + if (playlistItemIds.Contains(playingItem.PlaylistItemId)) + { + // Playing item has been removed, picking previous item. + PlayingItemIndex--; + if (PlayingItemIndex < 0) + { + // Was first element, picking next if available. + // Default to no playing item otherwise. + PlayingItemIndex = _sortedPlaylist.Count > 0 ? 0 : NoPlayingItemIndex; + } + + return true; + } + else + { + // Restoring playing item. + SetPlayingItemByPlaylistId(playingItem.PlaylistItemId); + return false; + } + } + else + { + return false; + } + } + + /// <summary> + /// Moves an item in the playlist to another position. + /// </summary> + /// <param name="playlistItemId">The item to move.</param> + /// <param name="newIndex">The new position.</param> + /// <returns><c>true</c> if the item has been moved; <c>false</c> otherwise.</returns> + public bool MovePlaylistItem(Guid playlistItemId, int newIndex) + { + var playlist = GetPlaylistInternal(); + var playingItem = GetPlayingItem(); + + var oldIndex = playlist.FindIndex(item => item.PlaylistItemId.Equals(playlistItemId)); + if (oldIndex < 0) + { + return false; + } + + var queueItem = playlist[oldIndex]; + playlist.RemoveAt(oldIndex); + newIndex = Math.Clamp(newIndex, 0, playlist.Count); + playlist.Insert(newIndex, queueItem); + + LastChange = DateTime.UtcNow; + PlayingItemIndex = playlist.IndexOf(playingItem); + return true; + } + + /// <summary> + /// Resets the playlist to its initial state. + /// </summary> + public void Reset() + { + _sortedPlaylist.Clear(); + _shuffledPlaylist.Clear(); + PlayingItemIndex = NoPlayingItemIndex; + ShuffleMode = GroupShuffleMode.Sorted; + RepeatMode = GroupRepeatMode.RepeatNone; + LastChange = DateTime.UtcNow; + } + + /// <summary> + /// Sets the repeat mode. + /// </summary> + /// <param name="mode">The new mode.</param> + public void SetRepeatMode(GroupRepeatMode mode) + { + RepeatMode = mode; + LastChange = DateTime.UtcNow; + } + + /// <summary> + /// Sets the shuffle mode. + /// </summary> + /// <param name="mode">The new mode.</param> + public void SetShuffleMode(GroupShuffleMode mode) + { + if (mode.Equals(GroupShuffleMode.Shuffle)) + { + ShufflePlaylist(); + } + else + { + RestoreSortedPlaylist(); + } + } + + /// <summary> + /// Toggles the shuffle mode between sorted and shuffled. + /// </summary> + public void ToggleShuffleMode() + { + if (ShuffleMode.Equals(GroupShuffleMode.Sorted)) + { + ShufflePlaylist(); + } + else + { + RestoreSortedPlaylist(); + } + } + + /// <summary> + /// Gets the next item in the playlist considering repeat mode and shuffle mode. + /// </summary> + /// <returns>The next item in the playlist.</returns> + public QueueItem GetNextItemPlaylistId() + { + int newIndex; + var playlist = GetPlaylistInternal(); + + switch (RepeatMode) + { + case GroupRepeatMode.RepeatOne: + newIndex = PlayingItemIndex; + break; + case GroupRepeatMode.RepeatAll: + newIndex = PlayingItemIndex + 1; + if (newIndex >= playlist.Count) + { + newIndex = 0; + } + + break; + default: + newIndex = PlayingItemIndex + 1; + break; + } + + if (newIndex < 0 || newIndex >= playlist.Count) + { + return null; + } + + return playlist[newIndex]; + } + + /// <summary> + /// Sets the next item in the queue as playing item. + /// </summary> + /// <returns><c>true</c> if the playing item changed; <c>false</c> otherwise.</returns> + public bool Next() + { + if (RepeatMode.Equals(GroupRepeatMode.RepeatOne)) + { + LastChange = DateTime.UtcNow; + return true; + } + + PlayingItemIndex++; + if (PlayingItemIndex >= _sortedPlaylist.Count) + { + if (RepeatMode.Equals(GroupRepeatMode.RepeatAll)) + { + PlayingItemIndex = 0; + } + else + { + PlayingItemIndex = _sortedPlaylist.Count - 1; + return false; + } + } + + LastChange = DateTime.UtcNow; + return true; + } + + /// <summary> + /// Sets the previous item in the queue as playing item. + /// </summary> + /// <returns><c>true</c> if the playing item changed; <c>false</c> otherwise.</returns> + public bool Previous() + { + if (RepeatMode.Equals(GroupRepeatMode.RepeatOne)) + { + LastChange = DateTime.UtcNow; + return true; + } + + PlayingItemIndex--; + if (PlayingItemIndex < 0) + { + if (RepeatMode.Equals(GroupRepeatMode.RepeatAll)) + { + PlayingItemIndex = _sortedPlaylist.Count - 1; + } + else + { + PlayingItemIndex = 0; + return false; + } + } + + LastChange = DateTime.UtcNow; + return true; + } + + /// <summary> + /// Creates a list from the array of items. Each item is given an unique playlist identifier. + /// </summary> + /// <returns>The list of queue items.</returns> + private List<QueueItem> CreateQueueItemsFromArray(IReadOnlyList<Guid> items) + { + var list = new List<QueueItem>(); + foreach (var item in items) + { + var queueItem = new QueueItem(item); + list.Add(queueItem); + } + + return list; + } + + /// <summary> + /// Gets the current playlist considering the shuffle mode. + /// </summary> + /// <returns>The playlist.</returns> + private List<QueueItem> GetPlaylistInternal() + { + if (ShuffleMode.Equals(GroupShuffleMode.Shuffle)) + { + return _shuffledPlaylist; + } + else + { + return _sortedPlaylist; + } + } + + /// <summary> + /// Gets the current playing item, depending on the shuffle mode. + /// </summary> + /// <returns>The playing item.</returns> + private QueueItem GetPlayingItem() + { + if (PlayingItemIndex == NoPlayingItemIndex) + { + return null; + } + else if (ShuffleMode.Equals(GroupShuffleMode.Shuffle)) + { + return _shuffledPlaylist[PlayingItemIndex]; + } + else + { + return _sortedPlaylist[PlayingItemIndex]; + } + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/Requests/JoinGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/Requests/JoinGroupRequest.cs new file mode 100644 index 000000000..38c9e8e20 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/Requests/JoinGroupRequest.cs @@ -0,0 +1,29 @@ +using System; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay.Requests +{ + /// <summary> + /// Class JoinGroupRequest. + /// </summary> + public class JoinGroupRequest : ISyncPlayRequest + { + /// <summary> + /// Initializes a new instance of the <see cref="JoinGroupRequest"/> class. + /// </summary> + /// <param name="groupId">The identifier of the group to join.</param> + public JoinGroupRequest(Guid groupId) + { + GroupId = groupId; + } + + /// <summary> + /// Gets the group identifier. + /// </summary> + /// <value>The identifier of the group to join.</value> + public Guid GroupId { get; } + + /// <inheritdoc /> + public RequestType Type { get; } = RequestType.JoinGroup; + } +} diff --git a/MediaBrowser.Controller/SyncPlay/Requests/LeaveGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/Requests/LeaveGroupRequest.cs new file mode 100644 index 000000000..545778264 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/Requests/LeaveGroupRequest.cs @@ -0,0 +1,13 @@ +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay.Requests +{ + /// <summary> + /// Class LeaveGroupRequest. + /// </summary> + public class LeaveGroupRequest : ISyncPlayRequest + { + /// <inheritdoc /> + public RequestType Type { get; } = RequestType.LeaveGroup; + } +} diff --git a/MediaBrowser.Controller/SyncPlay/Requests/ListGroupsRequest.cs b/MediaBrowser.Controller/SyncPlay/Requests/ListGroupsRequest.cs new file mode 100644 index 000000000..4a234fdab --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/Requests/ListGroupsRequest.cs @@ -0,0 +1,13 @@ +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay.Requests +{ + /// <summary> + /// Class ListGroupsRequest. + /// </summary> + public class ListGroupsRequest : ISyncPlayRequest + { + /// <inheritdoc /> + public RequestType Type { get; } = RequestType.ListGroups; + } +} diff --git a/MediaBrowser.Controller/SyncPlay/Requests/NewGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/Requests/NewGroupRequest.cs new file mode 100644 index 000000000..1321f0de8 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/Requests/NewGroupRequest.cs @@ -0,0 +1,28 @@ +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay.Requests +{ + /// <summary> + /// Class NewGroupRequest. + /// </summary> + public class NewGroupRequest : ISyncPlayRequest + { + /// <summary> + /// Initializes a new instance of the <see cref="NewGroupRequest"/> class. + /// </summary> + /// <param name="groupName">The name of the new group.</param> + public NewGroupRequest(string groupName) + { + GroupName = groupName; + } + + /// <summary> + /// Gets the group name. + /// </summary> + /// <value>The name of the new group.</value> + public string GroupName { get; } + + /// <inheritdoc /> + public RequestType Type { get; } = RequestType.NewGroup; + } +} diff --git a/MediaBrowser.Controller/TV/ITVSeriesManager.cs b/MediaBrowser.Controller/TV/ITVSeriesManager.cs index 09a0f6fea..e066c03fd 100644 --- a/MediaBrowser.Controller/TV/ITVSeriesManager.cs +++ b/MediaBrowser.Controller/TV/ITVSeriesManager.cs @@ -1,19 +1,31 @@ +#pragma warning disable CS1591 + using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Querying; namespace MediaBrowser.Controller.TV { + /// <summary> + /// The TV Series manager. + /// </summary> public interface ITVSeriesManager { /// <summary> /// Gets the next up. /// </summary> + /// <param name="query">The next up query.</param> + /// <param name="options">The dto options.</param> + /// <returns>The next up items.</returns> QueryResult<BaseItem> GetNextUp(NextUpQuery query, DtoOptions options); /// <summary> /// Gets the next up. /// </summary> + /// <param name="request">The next up request.</param> + /// <param name="parentsFolders">The list of parent folders.</param> + /// <param name="options">The dto options.</param> + /// <returns>The next up items.</returns> QueryResult<BaseItem> GetNextUp(NextUpQuery request, BaseItem[] parentsFolders, DtoOptions options); } } |
