diff options
Diffstat (limited to 'MediaBrowser.Controller')
113 files changed, 2294 insertions, 1080 deletions
diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs index d273b54fc6..ed7c2c2c17 100644 --- a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs +++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs @@ -35,7 +35,7 @@ namespace MediaBrowser.Controller.BaseItemManager public SemaphoreSlim MetadataRefreshThrottler { get; private set; } /// <inheritdoc /> - public bool IsMetadataFetcherEnabled(BaseItem baseItem, LibraryOptions libraryOptions, string name) + public bool IsMetadataFetcherEnabled(BaseItem baseItem, TypeOptions? libraryTypeOptions, string name) { if (baseItem is Channel) { @@ -49,19 +49,18 @@ namespace MediaBrowser.Controller.BaseItemManager return !baseItem.EnableMediaSourceDisplay; } - var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name); - if (typeOptions != null) + if (libraryTypeOptions is not null) { - return typeOptions.MetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); + return libraryTypeOptions.MetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); } var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, baseItem.GetType().Name, StringComparison.OrdinalIgnoreCase)); - return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); + return itemConfig is null || !itemConfig.DisabledMetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); } /// <inheritdoc /> - public bool IsImageFetcherEnabled(BaseItem baseItem, LibraryOptions libraryOptions, string name) + public bool IsImageFetcherEnabled(BaseItem baseItem, TypeOptions? libraryTypeOptions, string name) { if (baseItem is Channel) { @@ -75,15 +74,14 @@ namespace MediaBrowser.Controller.BaseItemManager return !baseItem.EnableMediaSourceDisplay; } - var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name); - if (typeOptions != null) + if (libraryTypeOptions is not null) { - return typeOptions.ImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); + return libraryTypeOptions.ImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); } var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, baseItem.GetType().Name, StringComparison.OrdinalIgnoreCase)); - return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); + return itemConfig is null || !itemConfig.DisabledImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); } /// <summary> diff --git a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs index e18994214e..b07c80879d 100644 --- a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs +++ b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs @@ -18,18 +18,18 @@ namespace MediaBrowser.Controller.BaseItemManager /// Is metadata fetcher enabled. /// </summary> /// <param name="baseItem">The base item.</param> - /// <param name="libraryOptions">The library options.</param> + /// <param name="libraryTypeOptions">The type options for <c>baseItem</c> from the library (if defined).</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); + bool IsMetadataFetcherEnabled(BaseItem baseItem, TypeOptions? libraryTypeOptions, string name); /// <summary> /// Is image fetcher enabled. /// </summary> /// <param name="baseItem">The base item.</param> - /// <param name="libraryOptions">The library options.</param> + /// <param name="libraryTypeOptions">The type options for <c>baseItem</c> from the library (if defined).</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); + bool IsImageFetcherEnabled(BaseItem baseItem, TypeOptions? libraryTypeOptions, string name); } } diff --git a/MediaBrowser.Controller/Channels/IChannelManager.cs b/MediaBrowser.Controller/Channels/IChannelManager.cs index 49be897ef3..8eb27888ab 100644 --- a/MediaBrowser.Controller/Channels/IChannelManager.cs +++ b/MediaBrowser.Controller/Channels/IChannelManager.cs @@ -16,12 +16,6 @@ namespace MediaBrowser.Controller.Channels public interface IChannelManager { /// <summary> - /// Adds the parts. - /// </summary> - /// <param name="channels">The channels.</param> - void AddParts(IEnumerable<IChannel> channels); - - /// <summary> /// Gets the channel features. /// </summary> /// <param name="id">The identifier.</param> @@ -52,14 +46,14 @@ namespace MediaBrowser.Controller.Channels /// </summary> /// <param name="query">The query.</param> /// <returns>The channels.</returns> - QueryResult<Channel> GetChannelsInternal(ChannelQuery query); + Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query); /// <summary> /// Gets the channels. /// </summary> /// <param name="query">The query.</param> /// <returns>The channels.</returns> - QueryResult<BaseItemDto> GetChannels(ChannelQuery query); + Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query); /// <summary> /// Gets the latest channel items. diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs index 11e6633011..7912c5e87e 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs @@ -42,8 +42,6 @@ namespace MediaBrowser.Controller.Drawing public IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; set; } - public bool AddPlayedIndicator { get; set; } - public int? UnplayedCount { get; set; } public int? Blur { get; set; } @@ -111,7 +109,6 @@ namespace MediaBrowser.Controller.Drawing { return (Quality >= 90) && IsFormatSupported(originalImagePath) && - !AddPlayedIndicator && PercentPlayed.Equals(0) && !UnplayedCount.HasValue && !Blur.HasValue && diff --git a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs index b036425ab3..62b70ce53c 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs @@ -18,7 +18,7 @@ namespace MediaBrowser.Controller.Drawing { var imageInfo = item.GetImageInfo(imageType, imageIndex); - if (imageInfo == null) + if (imageInfo is null) { return null; } diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs index 89aafc84fb..22453f0f76 100644 --- a/MediaBrowser.Controller/Dto/IDtoService.cs +++ b/MediaBrowser.Controller/Dto/IDtoService.cs @@ -1,4 +1,3 @@ -#nullable disable #pragma warning disable CA1002 using System.Collections.Generic; @@ -28,7 +27,7 @@ namespace MediaBrowser.Controller.Dto /// <param name="user">The user.</param> /// <param name="owner">The owner.</param> /// <returns>BaseItemDto.</returns> - BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null); + BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null); /// <summary> /// Gets the base item dtos. @@ -38,7 +37,7 @@ namespace MediaBrowser.Controller.Dto /// <param name="user">The user.</param> /// <param name="owner">The owner.</param> /// <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); + IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User? user = null, BaseItem? owner = null); /// <summary> /// Gets the item by name dto. @@ -48,6 +47,6 @@ namespace MediaBrowser.Controller.Dto /// <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); + 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 e671e5c711..d789033f16 100644 --- a/MediaBrowser.Controller/Entities/AggregateFolder.cs +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -67,14 +67,14 @@ namespace MediaBrowser.Controller.Entities { lock (_childIdsLock) { - if (_childrenIds == null || _childrenIds.Length == 0) + if (_childrenIds is null || _childrenIds.Length == 0) { var list = base.LoadChildren(); _childrenIds = list.Select(i => i.Id).ToArray(); return list; } - return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList(); + return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i is not null).ToList(); } } @@ -120,7 +120,7 @@ namespace MediaBrowser.Controller.Entities var path = ContainingFolderPath; - var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService) + var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, LibraryManager) { FileInfo = FileSystem.GetDirectoryInfo(path) }; diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index 29f7bf92b7..c7216a3209 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -81,8 +81,8 @@ namespace MediaBrowser.Controller.Entities.Audio /// <returns>System.String.</returns> protected override string CreateSortName() { - return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) - + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) + Name; + return (ParentIndexNumber is not null ? ParentIndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) + + (IndexNumber is not null ? IndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) + Name; } public override List<string> GetUserDataKeys() diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index 6555de8554..2dbd513a17 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -138,7 +138,7 @@ namespace MediaBrowser.Controller.Entities.Audio var artist = GetMusicArtist(new DtoOptions(false)); - if (artist != null) + if (artist is not null) { id.ArtistProviderIds = artist.ProviderIds; } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 7f5f9f74bd..1e868194e6 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -47,7 +47,7 @@ namespace MediaBrowser.Controller.Entities /// The supported image extensions. /// </summary> public static readonly string[] SupportedImageExtensions - = new[] { ".png", ".jpg", ".jpeg", ".tbn", ".gif" }; + = new[] { ".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif" }; private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions) { @@ -56,6 +56,7 @@ namespace MediaBrowser.Controller.Entities ".srt", ".vtt", ".sub", + ".sup", ".idx", ".txt", ".edl", @@ -128,6 +129,13 @@ namespace MediaBrowser.Controller.Entities public string Album { get; set; } /// <summary> + /// Gets or sets the LUFS value. + /// </summary> + /// <value>The LUFS Value.</value> + [JsonIgnore] + public float LUFS { get; set; } + + /// <summary> /// Gets or sets the channel identifier. /// </summary> /// <value>The channel identifier.</value> @@ -494,7 +502,7 @@ namespace MediaBrowser.Controller.Entities { get { - if (_sortName == null) + if (_sortName is null) { if (!string.IsNullOrEmpty(ForcedSortName)) { @@ -553,7 +561,7 @@ namespace MediaBrowser.Controller.Entities public string OfficialRating { get; set; } [JsonIgnore] - public int InheritedParentalRatingValue { get; set; } + public int? InheritedParentalRatingValue { get; set; } /// <summary> /// Gets or sets the critic rating. @@ -658,7 +666,7 @@ namespace MediaBrowser.Controller.Entities } var parent = DisplayParent; - if (parent != null) + if (parent is not null) { return parent.OfficialRatingForComparison; } @@ -679,7 +687,7 @@ namespace MediaBrowser.Controller.Entities } var parent = DisplayParent; - if (parent != null) + if (parent is not null) { return parent.CustomRatingForComparison; } @@ -800,16 +808,14 @@ namespace MediaBrowser.Controller.Entities { return allowed.Contains(ChannelId); } - else - { - var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders); - foreach (var folder in collectionFolders) + var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders); + + foreach (var folder in collectionFolders) + { + if (allowed.Contains(folder.Id)) { - if (allowed.Contains(folder.Id)) - { - return true; - } + return true; } } @@ -880,7 +886,7 @@ namespace MediaBrowser.Controller.Entities /// <returns>System.String.</returns> protected virtual string CreateSortName() { - if (Name == null) + if (Name is null) { return null; // some items may not have name filled in properly } @@ -892,16 +898,6 @@ namespace MediaBrowser.Controller.Entities 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 @@ -920,12 +916,22 @@ namespace MediaBrowser.Controller.Entities } } + 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); + } + return ModifySortChunks(sortable); } - internal static string ModifySortChunks(string name) + internal static string ModifySortChunks(ReadOnlySpan<char> name) { - void AppendChunk(StringBuilder builder, bool isDigitChunk, ReadOnlySpan<char> chunk) + static void AppendChunk(StringBuilder builder, bool isDigitChunk, ReadOnlySpan<char> chunk) { if (isDigitChunk && chunk.Length < 10) { @@ -935,7 +941,7 @@ namespace MediaBrowser.Controller.Entities builder.Append(chunk); } - if (name.Length == 0) + if (name.IsEmpty) { return string.Empty; } @@ -949,13 +955,13 @@ namespace MediaBrowser.Controller.Entities var isDigit = char.IsDigit(name[i]); if (isDigit != isDigitChunk) { - AppendChunk(builder, isDigitChunk, name.AsSpan(chunkStart, i - chunkStart)); + AppendChunk(builder, isDigitChunk, name.Slice(chunkStart, i - chunkStart)); chunkStart = i; isDigitChunk = isDigit; } } - AppendChunk(builder, isDigitChunk, name.AsSpan(chunkStart)); + AppendChunk(builder, isDigitChunk, name.Slice(chunkStart)); // logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString()); return builder.ToString().RemoveDiacritics(); @@ -976,7 +982,7 @@ namespace MediaBrowser.Controller.Entities { var parent = GetParent(); - while (parent != null) + while (parent is not null) { yield return parent; @@ -1073,7 +1079,7 @@ namespace MediaBrowser.Controller.Entities { var stream = i.VideoStream; - return stream == null || stream.Width == null ? 0 : stream.Width.Value; + return stream is null || stream.Width is null ? 0 : stream.Width.Value; }) .ToList(); } @@ -1114,7 +1120,7 @@ namespace MediaBrowser.Controller.Entities } var video = item as Video; - if (video != null) + if (video is not null) { info.IsoType = video.IsoType; info.VideoType = video.VideoType; @@ -1153,7 +1159,7 @@ namespace MediaBrowser.Controller.Entities info.SupportsDirectStream = MediaSourceManager.SupportsDirectStream(info.Path, info.Protocol); } - if (video != null && video.VideoType != VideoType.VideoFile) + if (video is not null && video.VideoType != VideoType.VideoFile) { info.SupportsDirectStream = false; } @@ -1330,7 +1336,7 @@ namespace MediaBrowser.Controller.Entities public void SetParent(Folder parent) { - ParentId = parent == null ? Guid.Empty : parent.Id; + ParentId = parent is null ? Guid.Empty : parent.Id; } /// <summary> @@ -1533,12 +1539,6 @@ namespace MediaBrowser.Controller.Entities } var maxAllowedRating = user.MaxParentalAgeRating; - - if (maxAllowedRating == null) - { - return true; - } - var rating = CustomRatingForComparison; if (string.IsNullOrEmpty(rating)) @@ -1548,12 +1548,13 @@ namespace MediaBrowser.Controller.Entities if (string.IsNullOrEmpty(rating)) { + Logger.LogDebug("{0} has no parental rating set.", Name); return !GetBlockUnratedValue(user); } var value = LocalizationManager.GetRatingLevel(rating); - // Could not determine the integer value + // Could not determine rating level if (!value.HasValue) { var isAllowed = !GetBlockUnratedValue(user); @@ -1566,7 +1567,7 @@ namespace MediaBrowser.Controller.Entities return isAllowed; } - return value.Value <= maxAllowedRating.Value; + return !maxAllowedRating.HasValue || value.Value <= maxAllowedRating.Value; } public int? GetInheritedParentalRatingValue() @@ -1606,6 +1607,12 @@ namespace MediaBrowser.Controller.Entities return false; } + var allowedTagsPreference = user.GetPreference(PreferenceKind.AllowedTags); + if (allowedTagsPreference.Any() && !allowedTagsPreference.Any(i => Tags.Contains(i, StringComparison.OrdinalIgnoreCase))) + { + return false; + } + return true; } @@ -1620,10 +1627,10 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Gets the block unrated value. + /// Gets a bool indicating if access to the unrated item is blocked or not. /// </summary> /// <param name="user">The configuration.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> + /// <returns><c>true</c> if blocked, <c>false</c> otherwise.</returns> protected virtual bool GetBlockUnratedValue(User user) { // Don't block plain folders that are unrated. Let the media underneath get blocked @@ -1692,7 +1699,7 @@ namespace MediaBrowser.Controller.Entities var itemById = LibraryManager.GetItemById(info.ItemId.Value); - if (itemById != null) + if (itemById is not null) { return itemById; } @@ -1701,7 +1708,7 @@ namespace MediaBrowser.Controller.Entities var item = FindLinkedChild(info); // If still null, log - if (item == null) + if (item is null) { // Don't keep searching over and over info.ItemId = Guid.Empty; @@ -1725,7 +1732,7 @@ namespace MediaBrowser.Controller.Entities var itemByPath = LibraryManager.FindByPath(path, null); - if (itemByPath == null) + if (itemByPath is null) { Logger.LogWarning("Unable to find linked item at path {0}", info.Path); } @@ -1737,7 +1744,7 @@ namespace MediaBrowser.Controller.Entities { var item = LibraryManager.GetItemById(info.LibraryItemId); - if (item == null) + if (item is null) { Logger.LogWarning("Unable to find linked item at path {0}", info.Path); } @@ -1755,10 +1762,7 @@ namespace MediaBrowser.Controller.Entities /// <exception cref="ArgumentNullException">Throws if name is null.</exception> public void AddStudio(string name) { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentNullException(nameof(name)); - } + ArgumentException.ThrowIfNullOrEmpty(name); var current = Studios; @@ -1791,10 +1795,7 @@ namespace MediaBrowser.Controller.Entities /// <exception cref="ArgumentNullException">Throwns if name is null.</exception> public void AddGenre(string name) { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentNullException(nameof(name)); - } + ArgumentException.ThrowIfNullOrEmpty(name); var genres = Genres; if (!genres.Contains(name, StringComparison.OrdinalIgnoreCase)) @@ -1892,7 +1893,7 @@ namespace MediaBrowser.Controller.Entities var existingImage = GetImageInfo(image.Type, index); - if (existingImage == null) + if (existingImage is null) { AddImage(image); } @@ -1915,7 +1916,7 @@ namespace MediaBrowser.Controller.Entities var image = GetImageInfo(type, index); - if (image == null) + if (image is null) { AddImage(GetImageInfo(file, type)); } @@ -1942,7 +1943,7 @@ namespace MediaBrowser.Controller.Entities { var info = GetImageInfo(type, index); - if (info == null) + if (info is null) { // Nothing to do return; @@ -2035,7 +2036,7 @@ namespace MediaBrowser.Controller.Entities { var chapter = ItemRepository.GetChapter(this, imageIndex); - if (chapter == null) + if (chapter is null) { return null; } @@ -2060,7 +2061,7 @@ namespace MediaBrowser.Controller.Entities { var artist = FindParent<MusicArtist>(); - if (artist != null) + if (artist is not null) { return artist.GetImages(imageType).ElementAtOrDefault(imageIndex); } @@ -2147,7 +2148,7 @@ namespace MediaBrowser.Controller.Entities foreach (var newImage in images) { - if (newImage == null) + if (newImage is null) { throw new ArgumentException("null image found in list"); } @@ -2155,7 +2156,7 @@ namespace MediaBrowser.Controller.Entities var existing = existingImages .Find(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase)); - if (existing == null) + if (existing is null) { newImageList.Add(newImage); } @@ -2241,7 +2242,7 @@ namespace MediaBrowser.Controller.Entities var info1 = GetImageInfo(type, index1); var info2 = GetImageInfo(type, index2); - if (info1 == null || info2 == null) + if (info1 is null || info2 is null) { // Nothing to do return Task.CompletedTask; @@ -2274,14 +2275,14 @@ namespace MediaBrowser.Controller.Entities { var userdata = UserDataManager.GetUserData(user, this); - return userdata != null && userdata.Played; + return userdata is not null && userdata.Played; } public bool IsFavoriteOrLiked(User user) { var userdata = UserDataManager.GetUserData(user, this); - return userdata != null && (userdata.IsFavorite || (userdata.Likes ?? false)); + return userdata is not null && (userdata.IsFavorite || (userdata.Likes ?? false)); } public virtual bool IsUnplayed(User user) @@ -2290,7 +2291,7 @@ namespace MediaBrowser.Controller.Entities var userdata = UserDataManager.GetUserData(user, this); - return userdata == null || !userdata.Played; + return userdata is null || !userdata.Played; } ItemLookupInfo IHasLookupInfo<ItemLookupInfo>.GetLookupInfo() @@ -2445,18 +2446,23 @@ namespace MediaBrowser.Controller.Entities // Try to retrieve it from the db. If we don't find it, use the resolved version var video = LibraryManager.GetItemById(id) as Video; - if (video == null) + if (video is null) { video = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Video; newOptions.ForceSave = true; } - if (video == null) + if (video is null) { return Task.FromResult(true); } + if (video.OwnerId.Equals(default)) + { + video.OwnerId = this.Id; + } + return RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken); } @@ -2511,7 +2517,7 @@ namespace MediaBrowser.Controller.Entities var item = this; - var inheritedParentalRatingValue = item.GetInheritedParentalRatingValue() ?? 0; + var inheritedParentalRatingValue = item.GetInheritedParentalRatingValue() ?? null; if (inheritedParentalRatingValue != item.InheritedParentalRatingValue) { item.InheritedParentalRatingValue = inheritedParentalRatingValue; @@ -2565,7 +2571,7 @@ namespace MediaBrowser.Controller.Entities { return ExtraIds .Select(LibraryManager.GetItemById) - .Where(i => i != null) + .Where(i => i is not null) .OrderBy(i => i.SortName); } @@ -2578,7 +2584,7 @@ namespace MediaBrowser.Controller.Entities { return ExtraIds .Select(LibraryManager.GetItemById) - .Where(i => i != null) + .Where(i => i is not null) .Where(i => i.ExtraType.HasValue && extraTypes.Contains(i.ExtraType.Value)) .OrderBy(i => i.SortName); } diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs index 948eb43754..615d236c73 100644 --- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs +++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs @@ -89,13 +89,13 @@ namespace MediaBrowser.Controller.Entities } var v = sourceProp.GetValue(source); - if (v == null) + if (v is null) { continue; } var p = destProps.Find(x => x.Name == sourceProp.Name); - if (p != null) + if (p is not null) { p.SetValue(dest, v); } diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index 7dc7f774d0..095b261c05 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -288,7 +288,7 @@ namespace MediaBrowser.Controller.Entities { var path = ContainingFolderPath; - var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService) + var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, LibraryManager) { FileInfo = FileSystem.GetDirectoryInfo(path), Parent = GetParent() as Folder, @@ -355,8 +355,7 @@ namespace MediaBrowser.Controller.Entities return PhysicalLocations .Where(i => !FileSystem.AreEqual(i, Path)) .SelectMany(i => GetPhysicalParents(i, rootChildren)) - .GroupBy(x => x.Id) - .Select(x => x.First()); + .DistinctBy(x => x.Id); } private IEnumerable<Folder> GetPhysicalParents(string path, List<Folder> rootChildren) diff --git a/MediaBrowser.Controller/Entities/Extensions.cs b/MediaBrowser.Controller/Entities/Extensions.cs index 1acb92fdc6..3005bee0a9 100644 --- a/MediaBrowser.Controller/Entities/Extensions.cs +++ b/MediaBrowser.Controller/Entities/Extensions.cs @@ -17,16 +17,11 @@ namespace MediaBrowser.Controller.Entities /// <param name="url">Trailer URL.</param> public static void AddTrailerUrl(this BaseItem item, string url) { - ArgumentNullException.ThrowIfNull(url); - - if (url.Length == 0) - { - throw new ArgumentException("String can't be empty", nameof(url)); - } + ArgumentException.ThrowIfNullOrEmpty(url); var current = item.RemoteTrailers.FirstOrDefault(i => string.Equals(i.Url, url, StringComparison.OrdinalIgnoreCase)); - if (current == null) + if (current is null) { var mediaUrl = new MediaUrl { diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 808f918104..84952295c4 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -483,7 +483,7 @@ namespace MediaBrowser.Controller.Entities } }); - if (container != null) + if (container is not null) { await RefreshAllMetadataForContainer(container, refreshOptions, innerProgress, cancellationToken).ConfigureAwait(false); } @@ -515,7 +515,7 @@ namespace MediaBrowser.Controller.Entities async () => { var series = container as Series; - if (series != null) + if (series is not null) { await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); } @@ -529,7 +529,7 @@ namespace MediaBrowser.Controller.Entities { var container = child as IMetadataContainer; - if (container != null) + if (container is not null) { await RefreshAllMetadataForContainer(container, refreshOptions, progress, cancellationToken).ConfigureAwait(false); } @@ -703,7 +703,7 @@ namespace MediaBrowser.Controller.Entities IEnumerable<BaseItem> items; Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager); - if (query.User == null) + if (query.User is null) { items = GetRecursiveChildren(filter); } @@ -730,7 +730,7 @@ namespace MediaBrowser.Controller.Entities return LibraryManager.GetItemsResult(query); } - private QueryResult<BaseItem> QueryWithPostFiltering2(InternalItemsQuery query) + protected QueryResult<BaseItem> QueryWithPostFiltering2(InternalItemsQuery query) { var startIndex = query.StartIndex; var limit = query.Limit; @@ -741,7 +741,7 @@ namespace MediaBrowser.Controller.Entities IEnumerable<BaseItem> itemsList = LibraryManager.GetItemList(query); var user = query.User; - if (user != null) + if (user is not null) { // needed for boxsets itemsList = itemsList.Where(i => i.IsVisibleStandalone(query.User)); @@ -961,7 +961,7 @@ namespace MediaBrowser.Controller.Entities IEnumerable<BaseItem> items; - if (query.User == null) + if (query.User is null) { items = Children.Where(filter); } @@ -984,7 +984,7 @@ namespace MediaBrowser.Controller.Entities var user = query.User; // Check recursive - don't substitute in plain folder views - if (user != null) + if (user is not null) { items = CollapseBoxSetItemsIfNeeded(items, query, this, user, ConfigurationManager, CollectionManager); } @@ -1069,7 +1069,7 @@ namespace MediaBrowser.Controller.Entities if (!param.HasValue) { - if (user != null && !configurationManager.Configuration.EnableGroupingIntoCollections) + if (user is not null && !configurationManager.Configuration.EnableGroupingIntoCollections) { return false; } @@ -1272,7 +1272,7 @@ namespace MediaBrowser.Controller.Entities { ArgumentNullException.ThrowIfNull(user); - return GetChildren(user, includeLinkedChildren, null); + return GetChildren(user, includeLinkedChildren, new InternalItemsQuery(user)); } public virtual List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) @@ -1300,8 +1300,15 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Adds the children to list. /// </summary> - private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query) + private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query, HashSet<Folder> visitedFolders = null) { + // Prevent infinite recursion of nested folders + visitedFolders ??= new HashSet<Folder>(); + if (!visitedFolders.Add(this)) + { + return; + } + // 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)) @@ -1311,47 +1318,38 @@ namespace MediaBrowser.Controller.Entities } // If there are not sub-folders, proceed as normal. - if (children == null) + if (children is null) { children = GetEligibleChildrenForRecursiveChildren(user); } - foreach (var child in children) + AddChildrenFromCollection(children, user, includeLinkedChildren, result, recursive, query, visitedFolders); + + if (includeLinkedChildren) { - bool? isVisibleToUser = null; + AddChildrenFromCollection(GetLinkedChildren(user), user, includeLinkedChildren, result, recursive, query, visitedFolders); + } + } - if (query == null || UserViewBuilder.FilterItem(child, query)) + private void AddChildrenFromCollection(IEnumerable<BaseItem> children, User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query, HashSet<Folder> visitedFolders) + { + foreach (var child in children) + { + if (!child.IsVisible(user)) { - isVisibleToUser = child.IsVisible(user); - - if (isVisibleToUser.Value) - { - result[child.Id] = child; - } + continue; } - if (isVisibleToUser ?? child.IsVisible(user)) + if (query is null || UserViewBuilder.FilterItem(child, query)) { - if (recursive && child.IsFolder) - { - var folder = (Folder)child; - - folder.AddChildren(user, includeLinkedChildren, result, true, query); - } + result[child.Id] = child; } - } - if (includeLinkedChildren) - { - foreach (var child in GetLinkedChildren(user)) + if (recursive && child.IsFolder) { - if (query == null || UserViewBuilder.FilterItem(child, query)) - { - if (child.IsVisible(user)) - { - result[child.Id] = child; - } - } + var folder = (Folder)child; + + folder.AddChildren(user, includeLinkedChildren, result, true, query, visitedFolders); } } } @@ -1402,7 +1400,7 @@ namespace MediaBrowser.Controller.Entities { foreach (var child in Children) { - if (filter == null || filter(child)) + if (filter is null || filter(child)) { result[child.Id] = child; } @@ -1420,7 +1418,7 @@ namespace MediaBrowser.Controller.Entities { foreach (var child in GetLinkedChildren()) { - if (filter == null || filter(child)) + if (filter is null || filter(child)) { result[child.Id] = child; } @@ -1441,7 +1439,7 @@ namespace MediaBrowser.Controller.Entities { var child = GetLinkedChild(i); - if (child != null) + if (child is not null) { list.Add(child); } @@ -1467,7 +1465,7 @@ namespace MediaBrowser.Controller.Entities var child = GetLinkedChild(i); - if (child != null && child.Id.Equals(itemId)) + if (child is not null && child.Id.Equals(itemId)) { return true; } @@ -1478,7 +1476,7 @@ namespace MediaBrowser.Controller.Entities public List<BaseItem> GetLinkedChildren(User user) { - if (!FilterLinkedChildrenPerUser || user == null) + if (!FilterLinkedChildrenPerUser || user is null) { return GetLinkedChildren(); } @@ -1504,7 +1502,7 @@ namespace MediaBrowser.Controller.Entities { var child = GetLinkedChild(i); - if (child == null) + if (child is null) { continue; } @@ -1547,7 +1545,7 @@ namespace MediaBrowser.Controller.Entities { return LinkedChildren .Select(i => new Tuple<LinkedChild, BaseItem>(i, GetLinkedChild(i))) - .Where(i => i.Item2 != null); + .Where(i => i.Item2 is not null); } protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) @@ -1605,7 +1603,7 @@ namespace MediaBrowser.Controller.Entities return null; } }) - .Where(i => i != null) + .Where(i => i is not null) .ToList(); var currentShortcutLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Shortcut).ToList(); @@ -1662,7 +1660,7 @@ namespace MediaBrowser.Controller.Entities { // The querying doesn't support virtual unaired var episode = item as Episode; - if (episode != null && episode.IsUnaired) + if (episode is not null && episode.IsUnaired) { continue; } @@ -1719,7 +1717,7 @@ namespace MediaBrowser.Controller.Entities return; } - if (itemDto != null) + if (itemDto is not null) { if (fields.ContainsField(ItemFields.RecursiveItemCount)) { diff --git a/MediaBrowser.Controller/Entities/IHasShares.cs b/MediaBrowser.Controller/Entities/IHasShares.cs deleted file mode 100644 index e6fa27703b..0000000000 --- a/MediaBrowser.Controller/Entities/IHasShares.cs +++ /dev/null @@ -1,11 +0,0 @@ -#nullable disable - -#pragma warning disable CA1819, CS1591 - -namespace MediaBrowser.Controller.Entities -{ - public interface IHasShares - { - Share[] Shares { get; set; } - } -} diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 1bf5285382..a51299284b 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -26,6 +26,7 @@ namespace MediaBrowser.Controller.Entities EnableTotalRecordCount = true; ExcludeArtistIds = Array.Empty<Guid>(); ExcludeInheritedTags = Array.Empty<string>(); + IncludeInheritedTags = Array.Empty<string>(); ExcludeItemIds = Array.Empty<Guid>(); ExcludeItemTypes = Array.Empty<BaseItemKind>(); ExcludeTags = Array.Empty<string>(); @@ -55,7 +56,7 @@ namespace MediaBrowser.Controller.Entities public InternalItemsQuery(User? user) : this() { - if (user != null) + if (user is not null) { SetUser(user); } @@ -95,6 +96,8 @@ namespace MediaBrowser.Controller.Entities public string[] ExcludeInheritedTags { get; set; } + public string[] IncludeInheritedTags { get; set; } + public IReadOnlyList<string> Genres { get; set; } public bool? IsSpecialSeason { get; set; } @@ -316,7 +319,7 @@ namespace MediaBrowser.Controller.Entities { set { - if (value == null) + if (value is null) { ParentId = Guid.Empty; ParentType = null; @@ -368,6 +371,7 @@ namespace MediaBrowser.Controller.Entities } ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags); + IncludeInheritedTags = user.GetPreference(PreferenceKind.AllowedTags); User = user; } diff --git a/MediaBrowser.Controller/Entities/ItemImageInfo.cs b/MediaBrowser.Controller/Entities/ItemImageInfo.cs index ea8555dbfe..0171af27cf 100644 --- a/MediaBrowser.Controller/Entities/ItemImageInfo.cs +++ b/MediaBrowser.Controller/Entities/ItemImageInfo.cs @@ -39,6 +39,6 @@ namespace MediaBrowser.Controller.Entities public string BlurHash { get; set; } [JsonIgnore] - public bool IsLocalFile => Path == null || !Path.StartsWith("http", StringComparison.OrdinalIgnoreCase); + public bool IsLocalFile => Path is null || !Path.StartsWith("http", StringComparison.OrdinalIgnoreCase); } } diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index 882abc9272..66210cb6c4 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -104,7 +104,7 @@ namespace MediaBrowser.Controller.Entities.Movies public override bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders) { - return true; + return user.HasPermission(PermissionKind.IsAdministrator) || user.HasPermission(PermissionKind.EnableCollectionManagement); } public override bool IsSaveLocalMetadataEnabled() diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index 77e70f8fbd..81f6248fac 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -22,7 +22,7 @@ namespace MediaBrowser.Controller.Entities.Movies /// <inheritdoc /> [JsonIgnore] public IReadOnlyList<Guid> SpecialFeatureIds => GetExtras() - .Where(extra => extra.ExtraType != null && extra is Video) + .Where(extra => extra.ExtraType is not null && extra is Video) .Select(extra => extra.Id) .ToArray(); @@ -33,9 +33,9 @@ namespace MediaBrowser.Controller.Entities.Movies .ToArray(); /// <summary> - /// Gets or sets the name of the TMDB collection. + /// Gets or sets the name of the TMDb collection. /// </summary> - /// <value>The name of the TMDB collection.</value> + /// <value>The name of the TMDb collection.</value> public string TmdbCollectionName { get; set; } [JsonIgnore] diff --git a/MediaBrowser.Controller/Entities/PeopleHelper.cs b/MediaBrowser.Controller/Entities/PeopleHelper.cs index 8571bfcea5..5292bd7727 100644 --- a/MediaBrowser.Controller/Entities/PeopleHelper.cs +++ b/MediaBrowser.Controller/Entities/PeopleHelper.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Entities @@ -12,48 +13,44 @@ namespace MediaBrowser.Controller.Entities public static void AddPerson(List<PersonInfo> people, PersonInfo person) { ArgumentNullException.ThrowIfNull(person); - - if (string.IsNullOrEmpty(person.Name)) - { - throw new ArgumentException("The person's name was empty or null.", nameof(person)); - } + ArgumentException.ThrowIfNullOrEmpty(person.Name); // Normalize if (string.Equals(person.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase)) { - person.Type = PersonType.GuestStar; + person.Type = PersonKind.GuestStar; } else if (string.Equals(person.Role, PersonType.Director, StringComparison.OrdinalIgnoreCase)) { - person.Type = PersonType.Director; + person.Type = PersonKind.Director; } else if (string.Equals(person.Role, PersonType.Producer, StringComparison.OrdinalIgnoreCase)) { - person.Type = PersonType.Producer; + person.Type = PersonKind.Producer; } else if (string.Equals(person.Role, PersonType.Writer, StringComparison.OrdinalIgnoreCase)) { - person.Type = PersonType.Writer; + person.Type = PersonKind.Writer; } // If the type is GuestStar and there's already an Actor entry, then update it to avoid dupes - if (string.Equals(person.Type, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase)) + if (person.Type == PersonKind.GuestStar) { - var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && p.Type.Equals(PersonType.Actor, StringComparison.OrdinalIgnoreCase)); + var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && p.Type == PersonKind.Actor); - if (existing != null) + if (existing is not null) { - existing.Type = PersonType.GuestStar; + existing.Type = PersonKind.GuestStar; MergeExisting(existing, person); return; } } - if (string.Equals(person.Type, PersonType.Actor, StringComparison.OrdinalIgnoreCase)) + if (person.Type == PersonKind.Actor) { // If the actor already exists without a role and we have one, fill it in - var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && (p.Type.Equals(PersonType.Actor, StringComparison.OrdinalIgnoreCase) || p.Type.Equals(PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))); - if (existing == null) + var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && (p.Type == PersonKind.Actor || p.Type == PersonKind.GuestStar)); + if (existing is null) { // Wasn't there - add it people.Add(person); @@ -72,11 +69,11 @@ namespace MediaBrowser.Controller.Entities else { var existing = people.FirstOrDefault(p => - string.Equals(p.Name, person.Name, StringComparison.OrdinalIgnoreCase) && - string.Equals(p.Type, person.Type, StringComparison.OrdinalIgnoreCase)); + string.Equals(p.Name, person.Name, StringComparison.OrdinalIgnoreCase) + && p.Type == person.Type); // Check for dupes based on the combination of Name and Type - if (existing == null) + if (existing is null) { people.Add(person); } diff --git a/MediaBrowser.Controller/Entities/PersonInfo.cs b/MediaBrowser.Controller/Entities/PersonInfo.cs index 2b689ae7e2..3df0b0b785 100644 --- a/MediaBrowser.Controller/Entities/PersonInfo.cs +++ b/MediaBrowser.Controller/Entities/PersonInfo.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Entities @@ -36,7 +37,7 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the type. /// </summary> /// <value>The type.</value> - public string Type { get; set; } + public PersonKind Type { get; set; } /// <summary> /// Gets or sets the ascending sort order. @@ -57,10 +58,6 @@ namespace MediaBrowser.Controller.Entities return Name; } - public bool IsType(string type) - { - return string.Equals(Type, type, StringComparison.OrdinalIgnoreCase) - || string.Equals(Role, type, StringComparison.OrdinalIgnoreCase); - } + public bool IsType(PersonKind type) => Type == type || string.Equals(type.ToString(), Role, StringComparison.OrdinalIgnoreCase); } } diff --git a/MediaBrowser.Controller/Entities/Share.cs b/MediaBrowser.Controller/Entities/Share.cs deleted file mode 100644 index 64f446eef2..0000000000 --- a/MediaBrowser.Controller/Entities/Share.cs +++ /dev/null @@ -1,13 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -namespace MediaBrowser.Controller.Entities -{ - public class Share - { - public string UserId { get; set; } - - public bool CanEdit { get; set; } - } -} diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 15b721fe63..597b4cecbc 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -136,7 +136,7 @@ namespace MediaBrowser.Controller.Entities.TV public string FindSeriesSortName() { var series = Series; - return series == null ? SeriesName : series.SortName; + return series is null ? SeriesName : series.SortName; } public override double GetDefaultPrimaryImageAspectRatio() @@ -155,7 +155,7 @@ namespace MediaBrowser.Controller.Entities.TV var list = base.GetUserDataKeys(); var series = Series; - if (series != null && ParentIndexNumber.HasValue && IndexNumber.HasValue) + if (series is not null && ParentIndexNumber.HasValue && IndexNumber.HasValue) { var seriesUserDataKeys = series.GetUserDataKeys(); var take = seriesUserDataKeys.Count; @@ -181,14 +181,14 @@ namespace MediaBrowser.Controller.Entities.TV public string FindSeriesPresentationUniqueKey() { var series = Series; - return series == null ? null : series.PresentationUniqueKey; + return series is null ? null : series.PresentationUniqueKey; } public string FindSeasonName() { var season = Season; - if (season == null) + if (season is null) { if (ParentIndexNumber.HasValue) { @@ -204,7 +204,7 @@ namespace MediaBrowser.Controller.Entities.TV public string FindSeriesName() { var series = Series; - return series == null ? SeriesName : series.Name; + return series is null ? SeriesName : series.Name; } public Guid FindSeasonId() @@ -212,11 +212,11 @@ namespace MediaBrowser.Controller.Entities.TV var season = FindParent<Season>(); // Episodes directly in series folder - if (season == null) + if (season is null) { var series = Series; - if (series != null && ParentIndexNumber.HasValue) + if (series is not null && ParentIndexNumber.HasValue) { var findNumber = ParentIndexNumber.Value; @@ -226,7 +226,7 @@ namespace MediaBrowser.Controller.Entities.TV } } - return season == null ? Guid.Empty : season.Id; + return season is null ? Guid.Empty : season.Id; } /// <summary> @@ -235,8 +235,8 @@ namespace MediaBrowser.Controller.Entities.TV /// <returns>System.String.</returns> protected override string CreateSortName() { - return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("000 - ", CultureInfo.InvariantCulture) : string.Empty) - + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) + Name; + return (ParentIndexNumber is not null ? ParentIndexNumber.Value.ToString("000 - ", CultureInfo.InvariantCulture) : string.Empty) + + (IndexNumber is not null ? IndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) + Name; } /// <summary> @@ -262,7 +262,7 @@ namespace MediaBrowser.Controller.Entities.TV public Guid FindSeriesId() { var series = FindParent<Series>(); - return series == null ? Guid.Empty : series.Id; + return series is null ? Guid.Empty : series.Id; } public override IEnumerable<Guid> GetAncestorIds() @@ -302,12 +302,17 @@ namespace MediaBrowser.Controller.Entities.TV var series = Series; - if (series != null) + if (series is not null) { id.SeriesProviderIds = series.ProviderIds; id.SeriesDisplayOrder = series.DisplayOrder; } + if (Season is not null) + { + id.SeasonProviderIds = Season.ProviderIds; + } + id.IsMissingEpisode = IsMissingEpisode; id.IndexNumberEnd = IndexNumberEnd; @@ -320,7 +325,7 @@ namespace MediaBrowser.Controller.Entities.TV if (!IsLocked) { - if (SourceType == SourceType.Library) + if (SourceType == SourceType.Library || SourceType == SourceType.LiveTV) { try { diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 599d35da64..0a040a3c28 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -64,7 +64,7 @@ namespace MediaBrowser.Controller.Entities.TV { var series = Series; - if (series != null) + if (series is not null) { return series.Path; } @@ -93,7 +93,7 @@ namespace MediaBrowser.Controller.Entities.TV public string FindSeriesSortName() { var series = Series; - return series == null ? SeriesName : series.SortName; + return series is null ? SeriesName : series.SortName; } public override List<string> GetUserDataKeys() @@ -101,7 +101,7 @@ namespace MediaBrowser.Controller.Entities.TV var list = base.GetUserDataKeys(); var series = Series; - if (series != null) + if (series is not null) { var newList = series.GetUserDataKeys(); var suffix = (IndexNumber ?? 0).ToString("000", CultureInfo.InvariantCulture); @@ -129,7 +129,7 @@ namespace MediaBrowser.Controller.Entities.TV if (IndexNumber.HasValue) { var series = Series; - if (series != null) + if (series is not null) { return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000", CultureInfo.InvariantCulture); } @@ -144,12 +144,12 @@ namespace MediaBrowser.Controller.Entities.TV /// <returns>System.String.</returns> protected override string CreateSortName() { - return IndexNumber != null ? IndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) : Name; + return IndexNumber is not null ? IndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) : Name; } protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) { - if (query.User == null) + if (query.User is null) { return base.GetItemsInternal(query); } @@ -208,13 +208,13 @@ namespace MediaBrowser.Controller.Entities.TV public string FindSeriesPresentationUniqueKey() { var series = Series; - return series == null ? null : series.PresentationUniqueKey; + return series is null ? null : series.PresentationUniqueKey; } public string FindSeriesName() { var series = Series; - return series == null ? SeriesName : series.Name; + return series is null ? SeriesName : series.Name; } public Guid FindSeriesId() @@ -233,7 +233,7 @@ namespace MediaBrowser.Controller.Entities.TV var series = Series; - if (series != null) + if (series is not null) { id.SeriesProviderIds = series.ProviderIds; } diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index d66802a64f..a49c1609d9 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -28,6 +28,7 @@ namespace MediaBrowser.Controller.Entities.TV public Series() { AirDays = Array.Empty<DayOfWeek>(); + SeasonNames = new Dictionary<int, string>(); } public DayOfWeek[] AirDays { get; set; } @@ -35,6 +36,9 @@ namespace MediaBrowser.Controller.Entities.TV public string AirTime { get; set; } [JsonIgnore] + public Dictionary<int, string> SeasonNames { get; set; } + + [JsonIgnore] public override bool SupportsAddingToPlaylist => true; [JsonIgnore] @@ -218,7 +222,7 @@ namespace MediaBrowser.Controller.Entities.TV query.IncludeItemTypes = new[] { BaseItemKind.Season }; query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }; - if (user != null && !user.DisplayMissingEpisodes) + if (user is not null && !user.DisplayMissingEpisodes) { query.IsMissing = false; } @@ -266,7 +270,7 @@ namespace MediaBrowser.Controller.Entities.TV DtoOptions = options }; - if (user == null || !user.DisplayMissingEpisodes) + if (user is null || !user.DisplayMissingEpisodes) { query.IsMissing = false; } @@ -283,7 +287,7 @@ namespace MediaBrowser.Controller.Entities.TV // This depends on settings for that series // When this happens, remove the duplicate from season 0 - return allEpisodes.GroupBy(i => i.Id).Select(x => x.First()).Reverse(); + return allEpisodes.DistinctBy(i => i.Id).Reverse(); } public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken) @@ -369,7 +373,7 @@ namespace MediaBrowser.Controller.Entities.TV OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, DtoOptions = options }; - if (user != null) + if (user is not null) { if (!user.DisplayMissingEpisodes) { @@ -384,7 +388,7 @@ namespace MediaBrowser.Controller.Entities.TV public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, IEnumerable<BaseItem> allSeriesEpisodes, DtoOptions options) { - if (allSeriesEpisodes == null) + if (allSeriesEpisodes is null) { return GetSeasonEpisodes(parentSeason, user, options); } @@ -426,7 +430,7 @@ namespace MediaBrowser.Controller.Entities.TV } var season = episodeItem.Season; - return season != null && string.Equals(GetUniqueSeriesKey(season), seasonPresentationKey, StringComparison.OrdinalIgnoreCase); + return season is not null && string.Equals(GetUniqueSeriesKey(season), seasonPresentationKey, StringComparison.OrdinalIgnoreCase); }); } @@ -448,7 +452,7 @@ namespace MediaBrowser.Controller.Entities.TV { var episode = i; - if (episode != null) + if (episode is not null) { var currentSeasonNumber = episode.AiredSeasonNumber; diff --git a/MediaBrowser.Controller/Entities/UserItemData.cs b/MediaBrowser.Controller/Entities/UserItemData.cs index 50ba9ef308..ecca440f05 100644 --- a/MediaBrowser.Controller/Entities/UserItemData.cs +++ b/MediaBrowser.Controller/Entities/UserItemData.cs @@ -105,7 +105,7 @@ namespace MediaBrowser.Controller.Entities { get { - if (Rating != null) + if (Rating is not null) { return Rating >= MinLikeValue; } diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs index e547db5231..69743b926c 100644 --- a/MediaBrowser.Controller/Entities/UserRootFolder.cs +++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs @@ -56,14 +56,14 @@ namespace MediaBrowser.Controller.Entities { lock (_childIdsLock) { - if (_childrenIds == null) + if (_childrenIds is null) { var list = base.LoadChildren(); _childrenIds = list.Select(i => i.Id).ToList(); return list; } - return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList(); + return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i is not null).ToList(); } } diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index f467a60384..c276ab463a 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -46,7 +46,7 @@ namespace MediaBrowser.Controller.Entities { var user = query.User; - // if (query.IncludeItemTypes != null && + // if (query.IncludeItemTypes is not null && // query.IncludeItemTypes.Length == 1 && // string.Equals(query.IncludeItemTypes[0], "Playlist", StringComparison.OrdinalIgnoreCase)) // { @@ -263,7 +263,7 @@ namespace MediaBrowser.Controller.Entities return null; } }) - .Where(i => i != null) + .Where(i => i is not null) .Select(i => GetUserViewWithName(SpecialFolder.MovieGenre, i.SortName, parent)); return GetResult(genres, query); @@ -391,7 +391,7 @@ namespace MediaBrowser.Controller.Entities return null; } }) - .Where(i => i != null) + .Where(i => i is not null) .Select(i => GetUserViewWithName(SpecialFolder.TvGenre, i.SortName, parent)); return GetResult(genres, query); @@ -559,7 +559,7 @@ namespace MediaBrowser.Controller.Entities var val = query.Is3D.Value; var video = item as Video; - if (video == null || val != video.Video3DFormat.HasValue) + if (video is null || val != video.Video3DFormat.HasValue) { return false; } @@ -689,7 +689,7 @@ namespace MediaBrowser.Controller.Entities var video = item as Video; - if (video == null || val != video.HasSubtitles) + if (video is null || val != video.HasSubtitles) { return false; } @@ -776,7 +776,7 @@ namespace MediaBrowser.Controller.Entities if (query.VideoTypes.Length > 0) { var video = item as Video; - if (video == null || !query.VideoTypes.Contains(video.VideoType)) + if (video is null || !query.VideoTypes.Contains(video.VideoType)) { return false; } @@ -791,7 +791,7 @@ namespace MediaBrowser.Controller.Entities if (query.StudioIds.Length > 0 && !query.StudioIds.Any(id => { var studioItem = libraryManager.GetItemById(id); - return studioItem != null && item.Studios.Contains(studioItem.Name, StringComparison.OrdinalIgnoreCase); + return studioItem is not null && item.Studios.Contains(studioItem.Name, StringComparison.OrdinalIgnoreCase); })) { return false; @@ -801,7 +801,7 @@ namespace MediaBrowser.Controller.Entities if (query.GenreIds.Count > 0 && !query.GenreIds.Any(id => { var genreItem = libraryManager.GetItemById(id); - return genreItem != null && item.Genres.Contains(genreItem.Name, StringComparison.OrdinalIgnoreCase); + return genreItem is not null && item.Genres.Contains(genreItem.Name, StringComparison.OrdinalIgnoreCase); })) { return false; @@ -913,7 +913,7 @@ namespace MediaBrowser.Controller.Entities { var episode = item as Episode; - if (episode == null) + if (episode is null) { return false; } @@ -929,7 +929,7 @@ namespace MediaBrowser.Controller.Entities private IEnumerable<BaseItem> GetMediaFolders(User user) { - if (user == null) + if (user is null) { return _libraryManager.RootFolder .Children @@ -945,14 +945,14 @@ namespace MediaBrowser.Controller.Entities private BaseItem[] GetMediaFolders(User user, IEnumerable<string> viewTypes) { - if (user == null) + if (user is null) { return GetMediaFolders(null) .Where(i => { var folder = i as ICollectionFolder; - return folder != null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase); + return folder is not null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase); }).ToArray(); } @@ -961,13 +961,13 @@ namespace MediaBrowser.Controller.Entities { var folder = i as ICollectionFolder; - return folder != null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase); + return folder is not null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase); }).ToArray(); } private BaseItem[] GetMediaFolders(Folder parent, User user, IEnumerable<string> viewTypes) { - if (parent == null || parent is UserView) + if (parent is null || parent is UserView) { return GetMediaFolders(user, viewTypes); } diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 5de2e0f50e..5b7abea10a 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -373,7 +373,7 @@ namespace MediaBrowser.Controller.Entities { return LinkedAlternateVersions .Select(GetLinkedChild) - .Where(i => i != null) + .Where(i => i is not null) .OfType<Video>() .OrderBy(i => i.SortName); } @@ -386,7 +386,7 @@ namespace MediaBrowser.Controller.Entities { return GetAdditionalPartIds() .Select(i => LibraryManager.GetItemById(i)) - .Where(i => i != null) + .Where(i => i is not null) .OfType<Video>() .OrderBy(i => i.SortName); } @@ -469,7 +469,7 @@ namespace MediaBrowser.Controller.Entities var localAlternates = GetLocalAlternateVersionIds() .Select(i => LibraryManager.GetItemById(i)) - .Where(i => i != null); + .Where(i => i is not null); foreach (var item in localAlternates) { @@ -542,7 +542,7 @@ namespace MediaBrowser.Controller.Entities return i.Item1 is Video video ? video.GetLocalAlternateVersionIds() : Enumerable.Empty<Guid>(); }) .Select(LibraryManager.GetItemById) - .Where(i => i != null) + .Where(i => i is not null) .ToList(); list.AddRange(localAlternates.Select(i => (i, MediaSourceType.Default))); diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs index 5a71102617..6c58064ce9 100644 --- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs +++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs @@ -60,6 +60,11 @@ namespace MediaBrowser.Controller.Extensions public const string UnixSocketPermissionsKey = "kestrel:socketPermissions"; /// <summary> + /// The cache size of the SQL database, see cache_size. + /// </summary> + public const string SqliteCacheSizeKey = "sqlite:cacheSize"; + + /// <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> @@ -73,7 +78,7 @@ namespace MediaBrowser.Controller.Extensions /// </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) + public static string? GetFFmpegProbeSize(this IConfiguration configuration) => configuration[FfmpegProbeSizeKey]; /// <summary> @@ -81,7 +86,7 @@ namespace MediaBrowser.Controller.Extensions /// </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) + public static string? GetFFmpegAnalyzeDuration(this IConfiguration configuration) => configuration[FfmpegAnalyzeDurationKey]; /// <summary> @@ -105,7 +110,7 @@ namespace MediaBrowser.Controller.Extensions /// </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) + public static string? GetUnixSocketPath(this IConfiguration configuration) => configuration[UnixSocketPathKey]; /// <summary> @@ -113,7 +118,15 @@ namespace MediaBrowser.Controller.Extensions /// </summary> /// <param name="configuration">The configuration to read the setting from.</param> /// <returns>The unix socket permissions.</returns> - public static string GetUnixSocketPermissions(this IConfiguration configuration) + public static string? GetUnixSocketPermissions(this IConfiguration configuration) => configuration[UnixSocketPermissionsKey]; + + /// <summary> + /// Gets the cache_size from the <see cref="IConfiguration" />. + /// </summary> + /// <param name="configuration">The configuration to read the setting from.</param> + /// <returns>The sqlite cache size.</returns> + public static int? GetSqliteCacheSize(this IConfiguration configuration) + => configuration.GetValue<int?>(SqliteCacheSizeKey); } } diff --git a/MediaBrowser.Controller/IO/FileData.cs b/MediaBrowser.Controller/IO/FileData.cs index 71feae536c..9f7a93ae2c 100644 --- a/MediaBrowser.Controller/IO/FileData.cs +++ b/MediaBrowser.Controller/IO/FileData.cs @@ -35,10 +35,7 @@ namespace MediaBrowser.Controller.IO int flattenFolderDepth = 0, bool resolveShortcuts = true) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } + ArgumentException.ThrowIfNullOrEmpty(path); ArgumentNullException.ThrowIfNull(args); diff --git a/MediaBrowser.Controller/IServerApplicationPaths.cs b/MediaBrowser.Controller/IServerApplicationPaths.cs index 1890dbb360..608286cd8a 100644 --- a/MediaBrowser.Controller/IServerApplicationPaths.cs +++ b/MediaBrowser.Controller/IServerApplicationPaths.cs @@ -51,24 +51,6 @@ namespace MediaBrowser.Controller string YearPath { get; } /// <summary> - /// 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. - /// </summary> - /// <value>The ratings path.</value> - string RatingsPath { get; } - - /// <summary> - /// Gets the media info images path. - /// </summary> - /// <value>The media info images path.</value> - string MediaInfoImagesPath { get; } - - /// <summary> /// Gets the path to the user configuration directory. /// </summary> /// <value>The user configuration directory path.</value> diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 5905c25a57..f34e3d68d9 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -429,10 +429,16 @@ namespace MediaBrowser.Controller.Library /// Gets the collection folders. /// </summary> /// <param name="item">The item.</param> - /// <returns>IEnumerable<Folder>.</returns> + /// <returns>The folders that contain the item.</returns> List<Folder> GetCollectionFolders(BaseItem item); - List<Folder> GetCollectionFolders(BaseItem item, List<Folder> allUserRootChildren); + /// <summary> + /// Gets the collection folders. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="allUserRootChildren">The root folders to consider.</param> + /// <returns>The folders that contain the item.</returns> + List<Folder> GetCollectionFolders(BaseItem item, IEnumerable<Folder> allUserRootChildren); LibraryOptions GetLibraryOptions(BaseItem item); diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index 993e3e18f9..6d6a532dba 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -47,14 +45,14 @@ namespace MediaBrowser.Controller.Library /// <param name="id">The id.</param> /// <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); + User? GetUserById(Guid id); /// <summary> /// Gets the name of the user by. /// </summary> /// <param name="name">The name.</param> /// <returns>User.</returns> - User GetUserByName(string name); + User? GetUserByName(string name); /// <summary> /// Renames the user. @@ -99,13 +97,6 @@ namespace MediaBrowser.Controller.Library Task ResetPassword(User user); /// <summary> - /// Resets the easy password. - /// </summary> - /// <param name="user">The user.</param> - /// <returns>Task.</returns> - Task ResetEasyPassword(User user); - - /// <summary> /// Changes the password. /// </summary> /// <param name="user">The user.</param> @@ -114,21 +105,12 @@ namespace MediaBrowser.Controller.Library Task ChangePassword(User user, string newPassword); /// <summary> - /// Changes the easy password. - /// </summary> - /// <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. /// </summary> /// <param name="user">The user.</param> /// <param name="remoteEndPoint">The remote end point.</param> /// <returns>UserDto.</returns> - UserDto GetUserDto(User user, string remoteEndPoint = null); + UserDto GetUserDto(User user, string? remoteEndPoint = null); /// <summary> /// Authenticates the user. @@ -139,7 +121,7 @@ namespace MediaBrowser.Controller.Library /// <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); + Task<User?> AuthenticateUser(string username, string password, string passwordSha1, string remoteEndPoint, bool isUserSession); /// <summary> /// Starts the forgot password process. diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs index 91d162b411..c701021679 100644 --- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs +++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs @@ -1,12 +1,11 @@ #nullable disable -#pragma warning disable CA1721, CA1819, CS1591 +#pragma warning disable CS1591 using System; using System.Collections.Generic; using System.Linq; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.IO; @@ -23,22 +22,20 @@ namespace MediaBrowser.Controller.Library /// </summary> private readonly IServerApplicationPaths _appPaths; + private readonly ILibraryManager _libraryManager; private LibraryOptions _libraryOptions; /// <summary> /// Initializes a new instance of the <see cref="ItemResolveArgs" /> class. /// </summary> /// <param name="appPaths">The app paths.</param> - /// <param name="directoryService">The directory service.</param> - public ItemResolveArgs(IServerApplicationPaths appPaths, IDirectoryService directoryService) + /// <param name="libraryManager">The library manager.</param> + public ItemResolveArgs(IServerApplicationPaths appPaths, ILibraryManager libraryManager) { _appPaths = appPaths; - DirectoryService = directoryService; + _libraryManager = libraryManager; } - // TODO remove dependencies as properties, they should be injected where it makes sense - public IDirectoryService DirectoryService { get; } - /// <summary> /// Gets or sets the file system children. /// </summary> @@ -47,7 +44,7 @@ namespace MediaBrowser.Controller.Library public LibraryOptions LibraryOptions { - get => _libraryOptions ??= Parent == null ? new LibraryOptions() : BaseItem.LibraryManager.GetLibraryOptions(Parent); + get => _libraryOptions ??= Parent is null ? new LibraryOptions() : _libraryManager.GetLibraryOptions(Parent); set => _libraryOptions = value; } @@ -119,7 +116,7 @@ namespace MediaBrowser.Controller.Library get { var paths = string.IsNullOrEmpty(Path) ? Array.Empty<string>() : new[] { Path }; - return AdditionalLocations == null ? paths : paths.Concat(AdditionalLocations).ToArray(); + return AdditionalLocations is null ? paths : paths.Concat(AdditionalLocations).ToArray(); } } @@ -130,13 +127,13 @@ namespace MediaBrowser.Controller.Library { var parent = Parent; - if (parent != null) + if (parent is not null) { var item = parent as T; // Just in case the user decided to nest episodes. // Not officially supported but in some cases we can handle it. - if (item == null) + if (item is null) { var parents = parent.GetParents(); foreach (var currentParent in parents) @@ -148,7 +145,7 @@ namespace MediaBrowser.Controller.Library } } - return item != null; + return item is not null; } return false; @@ -171,10 +168,7 @@ namespace MediaBrowser.Controller.Library /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c> or empty.</exception> public void AddAdditionalLocation(string path) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentException("The path was empty or null.", nameof(path)); - } + ArgumentException.ThrowIfNullOrEmpty(path); AdditionalLocations ??= new List<string>(); AdditionalLocations.Add(path); @@ -190,10 +184,7 @@ namespace MediaBrowser.Controller.Library /// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c> or empty.</exception> public FileSystemMetadata GetFileSystemEntryByName(string name) { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentException("The name was empty or null.", nameof(name)); - } + ArgumentException.ThrowIfNullOrEmpty(name); return GetFileSystemEntryByPath(System.IO.Path.Combine(Path, name)); } @@ -206,10 +197,7 @@ namespace MediaBrowser.Controller.Library /// <exception cref="ArgumentNullException">Throws if path is invalid.</exception> public FileSystemMetadata GetFileSystemEntryByPath(string path) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentException("The path was empty or null.", nameof(path)); - } + ArgumentException.ThrowIfNullOrEmpty(path); foreach (var file in FileSystemChildren) { @@ -240,21 +228,15 @@ namespace MediaBrowser.Controller.Library /// <summary> /// Gets the configured content type for the path. /// </summary> - /// <remarks> - /// This is subject to future refactoring as it relies on a static property in BaseItem. - /// </remarks> /// <returns>The configured content type.</returns> public string GetConfiguredContentType() { - return BaseItem.LibraryManager.GetConfiguredContentType(Path); + return _libraryManager.GetConfiguredContentType(Path); } /// <summary> /// Gets the file system children that do not hit the ignore file check. /// </summary> - /// <remarks> - /// This is subject to future refactoring as it relies on a static property in BaseItem. - /// </remarks> /// <returns>The file system children that are not ignored.</returns> public IEnumerable<FileSystemMetadata> GetActualFileSystemChildren() { @@ -262,7 +244,7 @@ namespace MediaBrowser.Controller.Library for (var i = 0; i < numberOfChildren; i++) { var child = FileSystemChildren[i]; - if (BaseItem.LibraryManager.IgnoreFile(child, Parent)) + if (_libraryManager.IgnoreFile(child, Parent)) { continue; } @@ -287,14 +269,14 @@ namespace MediaBrowser.Controller.Library /// <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 is not null) { - if (args.Path == null && Path == null) + if (args.Path is null && Path is null) { return true; } - return args.Path != null && BaseItem.FileSystem.AreEqual(args.Path, Path); + return args.Path is not null && BaseItem.FileSystem.AreEqual(args.Path, Path); } return false; diff --git a/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs index 7bc8fa5abd..6d2c3c3d29 100644 --- a/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs +++ b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -9,7 +7,7 @@ namespace MediaBrowser.Controller.Library { public static class LibraryManagerExtensions { - public static BaseItem GetItemById(this ILibraryManager manager, string id) + public static BaseItem? GetItemById(this ILibraryManager manager, string id) { return manager.GetItemById(new Guid(id)); } diff --git a/MediaBrowser.Controller/Library/NameExtensions.cs b/MediaBrowser.Controller/Library/NameExtensions.cs index 9d78b8b6c8..ee37fb2dc2 100644 --- a/MediaBrowser.Controller/Library/NameExtensions.cs +++ b/MediaBrowser.Controller/Library/NameExtensions.cs @@ -10,12 +10,11 @@ namespace MediaBrowser.Controller.Library public static class NameExtensions { public static IEnumerable<string> DistinctNames(this IEnumerable<string> names) - => names.GroupBy(RemoveDiacritics, StringComparer.OrdinalIgnoreCase) - .Select(x => x.First()); + => names.DistinctBy(RemoveDiacritics, StringComparer.OrdinalIgnoreCase); private static string RemoveDiacritics(string? name) { - if (name == null) + if (name is null) { return string.Empty; } diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index 46bdca3027..3b6a16dee3 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -97,7 +97,7 @@ namespace MediaBrowser.Controller.LiveTv /// <param name="query">The query.</param> /// <param name="options">The options.</param> /// <returns>A recording.</returns> - QueryResult<BaseItemDto> GetRecordings(RecordingQuery query, DtoOptions options); + Task<QueryResult<BaseItemDto>> GetRecordingsAsync(RecordingQuery query, DtoOptions options); /// <summary> /// Gets the timers. @@ -308,6 +308,6 @@ namespace MediaBrowser.Controller.LiveTv void AddInfoToRecordingDto(BaseItem item, BaseItemDto dto, ActiveRecordingInfo activeRecordingInfo, User user = null); - List<BaseItem> GetRecordingFolders(User user); + Task<BaseItem[]> GetRecordingFoldersAsync(User user); } } diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs index 335222da96..f11e3c8f68 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; using Jellyfin.Extensions; @@ -105,12 +106,9 @@ namespace MediaBrowser.Controller.LiveTv protected override string CreateSortName() { - if (!string.IsNullOrEmpty(Number)) + if (double.TryParse(Number, CultureInfo.InvariantCulture, out double number)) { - if (double.TryParse(Number, NumberStyles.Any, CultureInfo.InvariantCulture, out double number)) - { - return string.Format(CultureInfo.InvariantCulture, "{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); @@ -122,9 +120,7 @@ namespace MediaBrowser.Controller.LiveTv } public IEnumerable<BaseItem> GetTaggedItems() - { - return new List<BaseItem>(); - } + => Enumerable.Empty<BaseItem>(); public override List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution) { @@ -139,7 +135,7 @@ namespace MediaBrowser.Controller.LiveTv Path = Path, RunTimeTicks = RunTimeTicks, Type = MediaSourceType.Placeholder, - IsInfiniteStream = RunTimeTicks == null + IsInfiniteStream = RunTimeTicks is null }; list.Add(info); diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs index 6c4a5ea177..c721fb7785 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs @@ -197,10 +197,8 @@ namespace MediaBrowser.Controller.LiveTv { return 2.0 / 3; } - else - { - return 16.0 / 9; - } + + return 16.0 / 9; } public override string GetClientTypeName() @@ -246,7 +244,7 @@ namespace MediaBrowser.Controller.LiveTv var listings = GetListingsProviderInfo(); - if (listings != null) + if (listings is not null) { if (!string.IsNullOrEmpty(listings.MoviePrefix) && name.StartsWith(listings.MoviePrefix, StringComparison.OrdinalIgnoreCase)) { diff --git a/MediaBrowser.Controller/Lyrics/LyricMetadata.cs b/MediaBrowser.Controller/Lyrics/LyricMetadata.cs index 6091ede52a..c4f0334892 100644 --- a/MediaBrowser.Controller/Lyrics/LyricMetadata.cs +++ b/MediaBrowser.Controller/Lyrics/LyricMetadata.cs @@ -1,5 +1,3 @@ -using System; - namespace MediaBrowser.Controller.Lyrics; /// <summary> diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index d4e025a43e..69c0d26b6b 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -14,14 +14,14 @@ </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <TreatWarningsAsErrors>false</TreatWarningsAsErrors> + <CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors> </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" /> - <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" /> - <PackageReference Include="System.Threading.Tasks.Dataflow" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Binder" /> + <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" /> + <PackageReference Include="System.Threading.Tasks.Dataflow" /> </ItemGroup> <ItemGroup> @@ -35,7 +35,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> + <TargetFramework>net7.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <PublishRepositoryUrl>true</PublishRepositoryUrl> @@ -51,13 +51,13 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> + <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> - <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" /> </ItemGroup> </Project> diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 0ddb3be426..af540ecb89 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -38,7 +38,14 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly ISubtitleEncoder _subtitleEncoder; private readonly IConfiguration _config; private readonly Version _minKernelVersionAmdVkFmtModifier = new Version(5, 15); - private readonly Version _minKernelVersioni915Hang = new Version(5, 18); + // i915 hang was fixed by linux 6.2 (3f882f2) + private readonly Version _minKerneli915Hang = new Version(5, 18); + private readonly Version _maxKerneli915Hang = new Version(6, 1, 3); + private readonly Version _minFixedKernel60i915Hang = new Version(6, 0, 18); + + private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0); + private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0); + private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3); private static readonly string[] _videoProfilesH264 = new[] { @@ -58,6 +65,41 @@ namespace MediaBrowser.Controller.MediaEncoding "Main10" }; + private static readonly HashSet<string> _mp4ContainerNames = new(StringComparer.OrdinalIgnoreCase) + { + "mp4", + "m4a", + "m4p", + "m4b", + "m4r", + "m4v", + }; + + // Set max transcoding channels for encoders that can't handle more than a set amount of channels + // AAC, FLAC, ALAC, libopus, libvorbis encoders all support at least 8 channels + private static readonly Dictionary<string, int> _audioTranscodeChannelLookup = new(StringComparer.OrdinalIgnoreCase) + { + { "wmav2", 2 }, + { "libmp3lame", 2 }, + { "libfdk_aac", 6 }, + { "aac_at", 6 }, + { "ac3", 6 }, + { "eac3", 6 }, + { "dca", 6 }, + { "mlp", 6 }, + { "truehd", 6 }, + }; + + public static readonly string[] LosslessAudioCodecs = new string[] + { + "alac", + "ape", + "flac", + "mlp", + "truehd", + "wavpack" + }; + public EncodingHelper( IApplicationPaths appPaths, IMediaEncoder mediaEncoder, @@ -97,14 +139,10 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(hwType) && encodingOptions.EnableHardwareEncoding - && codecMap.ContainsKey(hwType)) + && codecMap.TryGetValue(hwType, out var preferredEncoder) + && _mediaEncoder.SupportsEncoder(preferredEncoder)) { - var preferredEncoder = codecMap[hwType]; - - if (_mediaEncoder.SupportsEncoder(preferredEncoder)) - { - return preferredEncoder; - } + return preferredEncoder; } } @@ -125,7 +163,8 @@ namespace MediaBrowser.Controller.MediaEncoding private bool IsVaapiFullSupported() { - return _mediaEncoder.SupportsHwaccel("vaapi") + return _mediaEncoder.SupportsHwaccel("drm") + && _mediaEncoder.SupportsHwaccel("vaapi") && _mediaEncoder.SupportsFilter("scale_vaapi") && _mediaEncoder.SupportsFilter("deinterlace_vaapi") && _mediaEncoder.SupportsFilter("tonemap_vaapi") @@ -162,7 +201,7 @@ namespace MediaBrowser.Controller.MediaEncoding private bool IsHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options) { - if (state.VideoStream == null + if (state.VideoStream is null || !options.EnableTonemapping || GetVideoColorBitDepth(state) != 10) { @@ -189,7 +228,7 @@ namespace MediaBrowser.Controller.MediaEncoding private bool IsVulkanHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options) { - if (state.VideoStream == null) + if (state.VideoStream is null) { return false; } @@ -202,7 +241,7 @@ namespace MediaBrowser.Controller.MediaEncoding private bool IsVaapiVppTonemapAvailable(EncodingJobInfo state, EncodingOptions options) { - if (state.VideoStream == null + if (state.VideoStream is null || !options.EnableVppTonemapping || GetVideoColorBitDepth(state) != 10) { @@ -520,7 +559,8 @@ namespace MediaBrowser.Controller.MediaEncoding { return Array.FindIndex(_videoProfilesH264, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase)); } - else if (string.Equals("hevc", videoCodec, StringComparison.OrdinalIgnoreCase)) + + if (string.Equals("hevc", videoCodec, StringComparison.OrdinalIgnoreCase)) { return Array.FindIndex(_videoProfilesH265, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase)); } @@ -530,9 +570,12 @@ namespace MediaBrowser.Controller.MediaEncoding public string GetInputPathArgument(EncodingJobInfo state) { - var mediaPath = state.MediaPath ?? string.Empty; - - return _mediaEncoder.GetInputArgument(mediaPath, state.MediaSource); + return state.MediaSource.VideoType switch + { + VideoType.Dvd => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistVobFiles(state.MediaPath, null).ToList(), state.MediaSource), + VideoType.BluRay => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistM2tsFiles(state.MediaPath).ToList(), state.MediaSource), + _ => _mediaEncoder.GetInputArgument(state.MediaPath, state.MediaSource) + }; } /// <summary> @@ -546,6 +589,12 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase)) { + // Use Apple's aac encoder if available as it provides best audio quality + if (_mediaEncoder.SupportsEncoder("aac_at")) + { + return "aac_at"; + } + // Use libfdk_aac for better audio quality if using custom build of FFmpeg which has fdk_aac support if (_mediaEncoder.SupportsEncoder("libfdk_aac")) { @@ -580,6 +629,11 @@ namespace MediaBrowser.Controller.MediaEncoding return "flac"; } + if (string.Equals(codec, "dts", StringComparison.OrdinalIgnoreCase)) + { + return "dca"; + } + return codec.ToLowerInvariant(); } @@ -605,6 +659,26 @@ namespace MediaBrowser.Controller.MediaEncoding deviceIndex); } + private string GetVulkanDeviceArgs(int deviceIndex, string deviceName, string srcDeviceAlias, string alias) + { + alias ??= VulkanAlias; + deviceIndex = deviceIndex >= 0 + ? deviceIndex + : 0; + var vendorOpts = string.IsNullOrEmpty(deviceName) + ? ":" + deviceIndex + : ":" + "\"" + deviceName + "\""; + var options = string.IsNullOrEmpty(srcDeviceAlias) + ? vendorOpts + : "@" + srcDeviceAlias; + + return string.Format( + CultureInfo.InvariantCulture, + " -init_hw_device vulkan={0}{1}", + alias, + options); + } + private string GetOpenclDeviceArgs(int deviceIndex, string deviceVendorName, string srcDeviceAlias, string alias) { alias ??= OpenclAlias; @@ -640,28 +714,43 @@ namespace MediaBrowser.Controller.MediaEncoding options); } - private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string alias) + private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string srcDeviceAlias, string alias) { alias ??= VaapiAlias; renderNodePath = renderNodePath ?? "/dev/dri/renderD128"; - var options = string.IsNullOrEmpty(driver) - ? renderNodePath - : ",driver=" + driver + (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver); + var driverOpts = string.IsNullOrEmpty(driver) + ? ":" + renderNodePath + : ":,driver=" + driver + (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver); + var options = string.IsNullOrEmpty(srcDeviceAlias) + ? driverOpts + : "@" + srcDeviceAlias; return string.Format( CultureInfo.InvariantCulture, - " -init_hw_device vaapi={0}:{1}", + " -init_hw_device vaapi={0}{1}", alias, options); } + private string GetDrmDeviceArgs(string renderNodePath, string alias) + { + alias ??= DrmAlias; + renderNodePath = renderNodePath ?? "/dev/dri/renderD128"; + + return string.Format( + CultureInfo.InvariantCulture, + " -init_hw_device drm={0}:{1}", + alias, + renderNodePath); + } + private string GetQsvDeviceArgs(string alias) { var arg = " -init_hw_device qsv=" + (alias ?? QsvAlias); if (OperatingSystem.IsLinux()) { // derive qsv from vaapi device - return GetVaapiDeviceArgs(null, "iHD", "i915", VaapiAlias) + arg + "@" + VaapiAlias; + return GetVaapiDeviceArgs(null, "iHD", "i915", null, VaapiAlias) + arg + "@" + VaapiAlias; } if (OperatingSystem.IsWindows()) @@ -682,9 +771,12 @@ namespace MediaBrowser.Controller.MediaEncoding public string GetGraphicalSubCanvasSize(EncodingJobInfo state) { - if (state.SubtitleStream != null + // DVBSUB and DVDSUB use the fixed canvas size 720x576 + if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode - && !state.SubtitleStream.IsTextSubtitleStream) + && !state.SubtitleStream.IsTextSubtitleStream + && !string.Equals(state.SubtitleStream.Codec, "DVBSUB", StringComparison.OrdinalIgnoreCase) + && !string.Equals(state.SubtitleStream.Codec, "DVDSUB", StringComparison.OrdinalIgnoreCase)) { var inW = state.VideoStream?.Width; var inH = state.VideoStream?.Height; @@ -752,47 +844,57 @@ namespace MediaBrowser.Controller.MediaEncoding if (_mediaEncoder.IsVaapiDeviceInteliHD) { - args.Append(GetVaapiDeviceArgs(null, "iHD", null, VaapiAlias)); + args.Append(GetVaapiDeviceArgs(null, "iHD", null, null, VaapiAlias)); } else if (_mediaEncoder.IsVaapiDeviceInteli965) { // Only override i965 since it has lower priority than iHD in libva lookup. Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME", "i965"); Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME_JELLYFIN", "i965"); - args.Append(GetVaapiDeviceArgs(null, "i965", null, VaapiAlias)); - } - else - { - args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, VaapiAlias)); + args.Append(GetVaapiDeviceArgs(null, "i965", null, null, VaapiAlias)); } - var filterDevArgs = GetFilterHwDeviceArgs(VaapiAlias); + var filterDevArgs = string.Empty; + var doOclTonemap = isHwTonemapAvailable && IsOpenclFullSupported(); - if (isHwTonemapAvailable && IsOpenclFullSupported()) + if (_mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965) { - if (_mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965) + if (doOclTonemap && !isVaapiDecoder) { - if (!isVaapiDecoder) - { - args.Append(GetOpenclDeviceArgs(0, null, VaapiAlias, OpenclAlias)); - filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias); - } + args.Append(GetOpenclDeviceArgs(0, null, VaapiAlias, OpenclAlias)); + filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias); + } + } + else if (_mediaEncoder.IsVaapiDeviceAmd) + { + if (IsVulkanFullSupported() + && _mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier + && Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier) + { + args.Append(GetDrmDeviceArgs(options.VaapiDevice, DrmAlias)); + args.Append(GetVaapiDeviceArgs(null, null, null, DrmAlias, VaapiAlias)); + args.Append(GetVulkanDeviceArgs(0, null, DrmAlias, VulkanAlias)); + + // libplacebo wants an explicitly set vulkan filter device. + filterDevArgs = GetFilterHwDeviceArgs(VulkanAlias); } - else if (_mediaEncoder.IsVaapiDeviceAmd) + else { - if (!IsVulkanFullSupported() - || !_mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier - || Environment.OSVersion.Version < _minKernelVersionAmdVkFmtModifier) + args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, null, VaapiAlias)); + filterDevArgs = GetFilterHwDeviceArgs(VaapiAlias); + + if (doOclTonemap) { + // ROCm/ROCr OpenCL runtime args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias)); filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias); } } - else - { - args.Append(GetOpenclDeviceArgs(0, null, null, OpenclAlias)); - filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias); - } + } + else if (doOclTonemap) + { + args.Append(GetOpenclDeviceArgs(0, null, null, OpenclAlias)); + filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias); } args.Append(filterDevArgs); @@ -928,18 +1030,30 @@ namespace MediaBrowser.Controller.MediaEncoding arg.Append(canvasArgs); } - arg.Append(" -i ") - .Append(GetInputPathArgument(state)); + if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay) + { + var tmpConcatPath = Path.Join(options.TranscodingTempPath, state.MediaSource.Id + ".concat"); + _mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath); + arg.Append(" -f concat -safe 0 -i ") + .Append(tmpConcatPath); + } + else + { + arg.Append(" -i ") + .Append(GetInputPathArgument(state)); + } // sub2video for external graphical subtitles - if (state.SubtitleStream != null + if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleStream.IsExternal) { var subtitlePath = state.SubtitleStream.Path; + var subtitleExtension = Path.GetExtension(subtitlePath); - if (string.Equals(Path.GetExtension(subtitlePath), ".sub", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(subtitleExtension, ".sub", StringComparison.OrdinalIgnoreCase) + || string.Equals(subtitleExtension, ".sup", StringComparison.OrdinalIgnoreCase)) { var idxFile = Path.ChangeExtension(subtitlePath, ".idx"); if (File.Exists(idxFile)) @@ -963,7 +1077,7 @@ namespace MediaBrowser.Controller.MediaEncoding arg.Append(" -i file:\"").Append(subtitlePath).Append('\"'); } - if (state.AudioStream != null && state.AudioStream.IsExternal) + if (state.AudioStream is not null && state.AudioStream.IsExternal) { // Also seek the external audio stream. var seekAudioParam = GetFastSeekCommandLineParameter(state, options, segmentContainer); @@ -977,7 +1091,7 @@ namespace MediaBrowser.Controller.MediaEncoding // Disable auto inserted SW scaler for HW decoders in case of changed resolution. var isSwDecoder = string.IsNullOrEmpty(GetHardwareVideoDecoder(state, options)); - if (!isSwDecoder) + if (!isSwDecoder && _mediaEncoder.EncoderVersion >= new Version(4, 4)) { arg.Append(" -autoscale 0"); } @@ -1021,19 +1135,19 @@ namespace MediaBrowser.Controller.MediaEncoding { return "-bsf:v h264_mp4toannexb"; } - else if (IsH265(stream)) + + if (IsH265(stream)) { return "-bsf:v hevc_mp4toannexb"; } - else if (IsAAC(stream)) + + if (IsAAC(stream)) { // Convert adts header(mpegts) to asc header(mp4). return "-bsf:a aac_adtstoasc"; } - else - { - return null; - } + + return null; } public static string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer) @@ -1066,7 +1180,7 @@ namespace MediaBrowser.Controller.MediaEncoding public string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec) { - if (state.OutputVideoBitrate == null) + if (state.OutputVideoBitrate is null) { return string.Empty; } @@ -1111,10 +1225,8 @@ namespace MediaBrowser.Controller.MediaEncoding { return FormattableString.Invariant($" -rc_mode CBR -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}"); } - else - { - return FormattableString.Invariant($" -rc_mode VBR -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}"); - } + + return FormattableString.Invariant($" -rc_mode VBR -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}"); } return FormattableString.Invariant($" -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}"); @@ -1122,7 +1234,7 @@ namespace MediaBrowser.Controller.MediaEncoding public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level) { - if (double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out double requestLevel)) + if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel)) { if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)) @@ -1176,24 +1288,6 @@ namespace MediaBrowser.Controller.MediaEncoding ":fontsdir='{0}'", _mediaEncoder.EscapeSubtitleFilterPath(fontPath)); - // TODO - // var fallbackFontPath = Path.Combine(_appPaths.ProgramDataPath, "fonts", "DroidSansFallback.ttf"); - // string fallbackFontParam = string.Empty; - - // if (!File.Exists(fallbackFontPath)) - // { - // _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(fallbackFontPath)); - // using (var stream = _assemblyInfo.GetManifestResourceStream(GetType(), GetType().Namespace + ".DroidSansFallback.ttf")) - // { - // using (var fileStream = new FileStream(fallbackFontPath, FileMode.Create, FileAccess.Write, FileShare.Read)) - // { - // stream.CopyTo(fileStream); - // } - // } - // } - - // fallbackFontParam = string.Format(CultureInfo.InvariantCulture, ":force_style='FontName=Droid Sans Fallback':fontsdir='{0}'", _mediaEncoder.EscapeSubtitleFilterPath(_fileSystem.GetDirectoryName(fallbackFontPath))); - if (state.SubtitleStream.IsExternal) { var charsetParam = string.Empty; @@ -1221,7 +1315,6 @@ namespace MediaBrowser.Controller.MediaEncoding alphaParam, sub2videoParam, fontParam, - // fallbackFontParam, setPtsParam); } @@ -1250,7 +1343,7 @@ namespace MediaBrowser.Controller.MediaEncoding var maxrate = request.MaxFramerate; - if (maxrate.HasValue && state.VideoStream != null) + if (maxrate.HasValue && state.VideoStream is not null) { var contentRate = state.VideoStream.AverageFrameRate ?? state.VideoStream.RealFrameRate; @@ -1336,13 +1429,13 @@ namespace MediaBrowser.Controller.MediaEncoding var param = string.Empty; // Tutorials: Enable Intel GuC / HuC firmware loading for Low Power Encoding. - // https://01.org/linuxgraphics/downloads/firmware + // https://01.org/group/43/downloads/firmware // https://wiki.archlinux.org/title/intel_graphics#Enable_GuC_/_HuC_firmware_loading // Intel Low Power Encoding can save unnecessary CPU-GPU synchronization, // which will reduce overhead in performance intensive tasks such as 4k transcoding and tonemapping. var intelLowPowerHwEncoding = false; - // Workaround for linux 5.18+ i915 hang at cost of performance. + // Workaround for linux 5.18 to 6.1.3 i915 hang at cost of performance. // https://github.com/intel/media-driver/issues/1456 var enableWaFori915Hang = false; @@ -1361,18 +1454,25 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) { - if (OperatingSystem.IsLinux() && Environment.OSVersion.Version >= _minKernelVersioni915Hang) + if (OperatingSystem.IsLinux()) { - var vidDecoder = GetHardwareVideoDecoder(state, encodingOptions) ?? string.Empty; - var isIntelDecoder = vidDecoder.Contains("qsv", StringComparison.OrdinalIgnoreCase) - || vidDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); - var doOclTonemap = _mediaEncoder.SupportsHwaccel("qsv") - && IsVaapiSupported(state) - && IsOpenclFullSupported() - && !IsVaapiVppTonemapAvailable(state, encodingOptions) - && IsHwTonemapAvailable(state, encodingOptions); + var ver = Environment.OSVersion.Version; + var isFixedKernel60 = ver.Major == 6 && ver.Minor == 0 && ver >= _minFixedKernel60i915Hang; + var isUnaffectedKernel = ver < _minKerneli915Hang || ver > _maxKerneli915Hang; - enableWaFori915Hang = isIntelDecoder && doOclTonemap; + if (!(isUnaffectedKernel || isFixedKernel60)) + { + var vidDecoder = GetHardwareVideoDecoder(state, encodingOptions) ?? string.Empty; + var isIntelDecoder = vidDecoder.Contains("qsv", StringComparison.OrdinalIgnoreCase) + || vidDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); + var doOclTonemap = _mediaEncoder.SupportsHwaccel("qsv") + && IsVaapiSupported(state) + && IsOpenclFullSupported() + && !IsVaapiVppTonemapAvailable(state, encodingOptions) + && IsHwTonemapAvailable(state, encodingOptions); + + enableWaFori915Hang = isIntelDecoder && doOclTonemap; + } } if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)) @@ -1463,11 +1563,11 @@ namespace MediaBrowser.Controller.MediaEncoding param += " -preset p7"; break; - case "slow": + case "slower": param += " -preset p6"; break; - case "slower": + case "slow": param += " -preset p5"; break; @@ -1500,8 +1600,8 @@ namespace MediaBrowser.Controller.MediaEncoding switch (encodingOptions.EncoderPreset) { case "veryslow": - case "slow": case "slower": + case "slow": param += " -quality quality"; break; @@ -1717,7 +1817,7 @@ namespace MediaBrowser.Controller.MediaEncoding 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)) + if (double.TryParse(level, CultureInfo.InvariantCulture, out double hevcLevel)) { param += " -level " + (hevcLevel / 3); } @@ -1896,8 +1996,7 @@ namespace MediaBrowser.Controller.MediaEncoding // If a specific level was requested, the source must match or be less than var level = state.GetRequestedLevel(videoStream.Codec); - if (!string.IsNullOrEmpty(level) - && double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out var requestLevel)) + if (double.TryParse(level, CultureInfo.InvariantCulture, out var requestLevel)) { if (!videoStream.Level.HasValue) { @@ -1974,9 +2073,9 @@ namespace MediaBrowser.Controller.MediaEncoding } } - // Video bitrate must fall within requested value + // Audio bitrate must fall within requested value if (request.AudioBitRate.HasValue - && audioStream.BitDepth.HasValue + && audioStream.BitRate.HasValue && audioStream.BitRate.Value > request.AudioBitRate.Value) { return false; @@ -1989,7 +2088,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var bitrate = request.VideoBitRate; - if (videoStream != null) + if (videoStream is not null) { var isUpscaling = request.Height.HasValue && videoStream.Height.HasValue @@ -2040,14 +2139,20 @@ namespace MediaBrowser.Controller.MediaEncoding private static double GetVideoBitrateScaleFactor(string codec) { + // hevc & vp9 - 40% more efficient than h.264 if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase)) + || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase)) { return .6; } + // av1 - 50% more efficient than h.264 + if (string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase)) + { + return .5; + } + return 1; } @@ -2055,7 +2160,9 @@ namespace MediaBrowser.Controller.MediaEncoding { var inputScaleFactor = GetVideoBitrateScaleFactor(inputVideoCodec); var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec); - var scaleFactor = outputScaleFactor / inputScaleFactor; + + // Don't scale the real bitrate lower than the requested bitrate + var scaleFactor = Math.Max(outputScaleFactor / inputScaleFactor, 1); if (bitrate <= 500000) { @@ -2077,56 +2184,96 @@ namespace MediaBrowser.Controller.MediaEncoding return Convert.ToInt32(scaleFactor * bitrate); } - public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream) + public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream, int? outputAudioChannels) { - return GetAudioBitrateParam(request.AudioBitRate, request.AudioCodec, audioStream); + return GetAudioBitrateParam(request.AudioBitRate, request.AudioCodec, audioStream, outputAudioChannels); } - public int? GetAudioBitrateParam(int? audioBitRate, string audioCodec, MediaStream audioStream) + public int? GetAudioBitrateParam(int? audioBitRate, string audioCodec, MediaStream audioStream, int? outputAudioChannels) { - if (audioStream == null) + if (audioStream is null) { return null; } - if (audioBitRate.HasValue && string.IsNullOrEmpty(audioCodec)) + var inputChannels = audioStream.Channels ?? 0; + var outputChannels = outputAudioChannels ?? 0; + var bitrate = audioBitRate ?? int.MaxValue; + + if (string.IsNullOrEmpty(audioCodec) + || string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase) + || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase) + || string.Equals(audioCodec, "opus", StringComparison.OrdinalIgnoreCase) + || string.Equals(audioCodec, "vorbis", StringComparison.OrdinalIgnoreCase) + || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase) + || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase)) { - return Math.Min(384000, audioBitRate.Value); + return (inputChannels, outputChannels) switch + { + (>= 6, >= 6 or 0) => Math.Min(640000, bitrate), + (> 0, > 0) => Math.Min(outputChannels * 128000, bitrate), + (> 0, _) => Math.Min(inputChannels * 128000, bitrate), + (_, _) => Math.Min(384000, bitrate) + }; } - if (audioBitRate.HasValue && !string.IsNullOrEmpty(audioCodec)) + if (string.Equals(audioCodec, "dts", StringComparison.OrdinalIgnoreCase) + || string.Equals(audioCodec, "dca", StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase) - || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase) - || string.Equals(audioCodec, "opus", StringComparison.OrdinalIgnoreCase) - || string.Equals(audioCodec, "vorbis", StringComparison.OrdinalIgnoreCase) - || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase) - || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase)) + return (inputChannels, outputChannels) switch { - if ((audioStream.Channels ?? 0) >= 6) - { - return Math.Min(640000, audioBitRate.Value); - } + (>= 6, >= 6 or 0) => Math.Min(768000, bitrate), + (> 0, > 0) => Math.Min(outputChannels * 136000, bitrate), + (> 0, _) => Math.Min(inputChannels * 136000, bitrate), + (_, _) => Math.Min(672000, bitrate) + }; + } - return Math.Min(384000, audioBitRate.Value); - } + // Empty bitrate area is not allow on iOS + // Default audio bitrate to 128K per channel if we don't have codec specific defaults + // https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options + return 128000 * (outputAudioChannels ?? audioStream.Channels ?? 2); + } - if (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase) - || string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase)) + public string GetAudioVbrModeParam(string encoder, int bitratePerChannel) + { + if (string.Equals(encoder, "libfdk_aac", StringComparison.OrdinalIgnoreCase)) + { + return " -vbr:a " + bitratePerChannel switch { - if ((audioStream.Channels ?? 0) >= 6) - { - return Math.Min(3584000, audioBitRate.Value); - } + < 32000 => "1", + < 48000 => "2", + < 64000 => "3", + < 96000 => "4", + _ => "5" + }; + } - return Math.Min(1536000, audioBitRate.Value); - } + if (string.Equals(encoder, "libmp3lame", StringComparison.OrdinalIgnoreCase)) + { + return " -qscale:a " + bitratePerChannel switch + { + < 48000 => "8", + < 64000 => "6", + < 88000 => "4", + < 112000 => "2", + _ => "0" + }; } - // 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; + if (string.Equals(encoder, "libvorbis", StringComparison.OrdinalIgnoreCase)) + { + return " -qscale:a " + bitratePerChannel switch + { + < 40000 => "0", + < 56000 => "2", + < 80000 => "4", + < 112000 => "6", + _ => "8" + }; + } + + return null; } public string GetAudioFilterParam(EncodingJobInfo state, EncodingOptions encodingOptions) @@ -2135,19 +2282,34 @@ namespace MediaBrowser.Controller.MediaEncoding var filters = new List<string>(); - // Boost volume to 200% when downsampling from 6ch to 2ch if (channels.HasValue - && channels.Value <= 2 - && state.AudioStream != null + && channels.Value == 2 + && state.AudioStream is not null && state.AudioStream.Channels.HasValue - && state.AudioStream.Channels.Value > 5 - && !encodingOptions.DownMixAudioBoost.Equals(1)) + && state.AudioStream.Channels.Value > 5) { - filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture)); + switch (encodingOptions.DownMixStereoAlgorithm) + { + case DownMixStereoAlgorithms.Dave750: + filters.Add("volume=4.25"); + filters.Add("pan=stereo|c0=0.5*c2+0.707*c0+0.707*c4+0.5*c3|c1=0.5*c2+0.707*c1+0.707*c5+0.5*c3"); + break; + case DownMixStereoAlgorithms.NightmodeDialogue: + filters.Add("pan=stereo|c0=c2+0.30*c0+0.30*c4|c1=c2+0.30*c1+0.30*c5"); + break; + case DownMixStereoAlgorithms.None: + default: + if (!encodingOptions.DownMixAudioBoost.Equals(1)) + { + filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture)); + } + + break; + } } var isCopyingTimestamps = state.CopyTimestamps || state.TranscodingType != TranscodingJobType.Progressive; - if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode && !isCopyingTimestamps) + if (state.SubtitleStream is not null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode && !isCopyingTimestamps) { var seconds = TimeSpan.FromTicks(state.StartTimeTicks ?? 0).TotalSeconds; @@ -2175,94 +2337,55 @@ namespace MediaBrowser.Controller.MediaEncoding /// <returns>System.Nullable{System.Int32}.</returns> public int? GetNumAudioChannelsParam(EncodingJobInfo state, MediaStream audioStream, string outputAudioCodec) { - if (audioStream == null) + if (audioStream is null) { return null; } var request = state.BaseRequest; - var inputChannels = audioStream.Channels; + var codec = outputAudioCodec ?? string.Empty; - if (inputChannels <= 0) - { - inputChannels = null; - } + int? resultChannels = state.GetRequestedAudioChannels(codec); - var codec = outputAudioCodec ?? string.Empty; + var inputChannels = audioStream.Channels; - 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 (inputChannels > 0) { - // 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; + resultChannels = inputChannels < resultChannels ? inputChannels : resultChannels ?? inputChannels; } var isTranscodingAudio = !IsCopyCodec(codec); - int? resultChannels = state.GetRequestedAudioChannels(codec); if (isTranscodingAudio) { - resultChannels = GetMinValue(request.TranscodingMaxAudioChannels, resultChannels); - } + var audioEncoder = GetAudioEncoder(state); + if (!_audioTranscodeChannelLookup.TryGetValue(audioEncoder, out var transcoderChannelLimit)) + { + // Set default max transcoding channels to 8 to prevent encoding errors due to asking for too many channels. + transcoderChannelLimit = 8; + } - if (inputChannels.HasValue) - { - resultChannels = resultChannels.HasValue - ? Math.Min(resultChannels.Value, inputChannels.Value) - : inputChannels.Value; - } + // Set resultChannels to minimum between resultChannels, TranscodingMaxAudioChannels, transcoderChannelLimit + resultChannels = transcoderChannelLimit < resultChannels ? transcoderChannelLimit : resultChannels ?? transcoderChannelLimit; - if (isTranscodingAudio && transcoderChannelLimit.HasValue) - { - resultChannels = resultChannels.HasValue - ? Math.Min(resultChannels.Value, transcoderChannelLimit.Value) - : transcoderChannelLimit.Value; - } + if (request.TranscodingMaxAudioChannels < resultChannels) + { + resultChannels = request.TranscodingMaxAudioChannels; + } - // 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; + // 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 (state.TranscodingType != TranscodingJobType.Progressive + && ((resultChannels > 2 && resultChannels < 6) || resultChannels == 7)) + { + resultChannels = 2; + } } return resultChannels; } - private int? GetMinValue(int? val1, int? val2) - { - if (!val1.HasValue) - { - return val2; - } - - if (!val2.HasValue) - { - return val1; - } - - return Math.Min(val1.Value, val2.Value); - } - /// <summary> /// Enforces the resolution limit. /// </summary> @@ -2328,26 +2451,26 @@ namespace MediaBrowser.Controller.MediaEncoding // If we don't have known media info // If input is video, use -sn to drop subtitles // Otherwise just return empty - if (state.VideoStream == null && state.AudioStream == null) + if (state.VideoStream is null && state.AudioStream is null) { return state.IsInputVideo ? "-sn" : string.Empty; } // We have media info, but we don't know the stream index - if (state.VideoStream != null && state.VideoStream.Index == -1) + if (state.VideoStream is not null && state.VideoStream.Index == -1) { return "-sn"; } // We have media info, but we don't know the stream index - if (state.AudioStream != null && state.AudioStream.Index == -1) + if (state.AudioStream is not null && state.AudioStream.Index == -1) { return state.IsInputVideo ? "-sn" : string.Empty; } var args = string.Empty; - if (state.VideoStream != null) + if (state.VideoStream is not null) { int videoStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.VideoStream); @@ -2362,12 +2485,12 @@ namespace MediaBrowser.Controller.MediaEncoding args += "-vn"; } - if (state.AudioStream != null) + if (state.AudioStream is not null) { int audioStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.AudioStream); if (state.AudioStream.IsExternal) { - bool hasExternalGraphicsSubs = state.SubtitleStream != null + bool hasExternalGraphicsSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream; @@ -2393,7 +2516,7 @@ namespace MediaBrowser.Controller.MediaEncoding } var subtitleMethod = state.SubtitleDeliveryMethod; - if (state.SubtitleStream == null || subtitleMethod == SubtitleDeliveryMethod.Hls) + if (state.SubtitleStream is null || subtitleMethod == SubtitleDeliveryMethod.Hls) { args += " -map -0:s"; } @@ -2420,6 +2543,30 @@ namespace MediaBrowser.Controller.MediaEncoding } /// <summary> + /// Gets the negative map args by filters. + /// </summary> + /// <param name="state">The state.</param> + /// <param name="videoProcessFilters">The videoProcessFilters.</param> + /// <returns>System.String.</returns> + public string GetNegativeMapArgsByFilters(EncodingJobInfo state, string videoProcessFilters) + { + string args = string.Empty; + + // http://ffmpeg.org/ffmpeg-all.html#toc-Complex-filtergraphs-1 + if (state.VideoStream != null && videoProcessFilters.Contains("-filter_complex", StringComparison.Ordinal)) + { + int videoStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.VideoStream); + + args += string.Format( + CultureInfo.InvariantCulture, + "-map -0:{0} ", + videoStreamIndex); + } + + return args; + } + + /// <summary> /// Determines which stream will be used for playback. /// </summary> /// <param name="allStream">All stream.</param> @@ -2435,7 +2582,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var stream = streams.FirstOrDefault(s => s.Index == desiredIndex.Value); - if (stream != null) + if (stream is not null) { return stream; } @@ -2480,8 +2627,8 @@ namespace MediaBrowser.Controller.MediaEncoding if (outputWidth > maximumWidth || outputHeight > maximumHeight) { - var scaleW = (double)maximumWidth / (double)outputWidth; - var scaleH = (double)maximumHeight / (double)outputHeight; + var scaleW = (double)maximumWidth / outputWidth; + var scaleH = (double)maximumHeight / outputHeight; var scale = Math.Min(scaleW, scaleH); outputWidth = Math.Min(maximumWidth, (int)(outputWidth * scale)); outputHeight = Math.Min(maximumHeight, (int)(outputHeight * scale)); @@ -2628,79 +2775,76 @@ namespace MediaBrowser.Controller.MediaEncoding widthParam, heightParam); } - else - { - return GetFixedSwScaleFilter(threedFormat, requestedWidth.Value, requestedHeight.Value); - } + + return GetFixedSwScaleFilter(threedFormat, requestedWidth.Value, requestedHeight.Value); } // 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) + + if (requestedMaxWidth.HasValue && requestedMaxHeight.HasValue) { var maxWidthParam = requestedMaxWidth.Value.ToString(CultureInfo.InvariantCulture); var maxHeightParam = requestedMaxHeight.Value.ToString(CultureInfo.InvariantCulture); return string.Format( - CultureInfo.InvariantCulture, - "scale=trunc(min(max(iw\\,ih*a)\\,min({0}\\,{1}*a))/{2})*{2}:trunc(min(max(iw/a\\,ih)\\,min({0}/a\\,{1}))/2)*2", - maxWidthParam, - maxHeightParam, - scaleVal); + CultureInfo.InvariantCulture, + "scale=trunc(min(max(iw\\,ih*a)\\,min({0}\\,{1}*a))/{2})*{2}:trunc(min(max(iw/a\\,ih)\\,min({0}/a\\,{1}))/2)*2", + maxWidthParam, + maxHeightParam, + scaleVal); } // If a fixed width was requested - else if (requestedWidth.HasValue) + if (requestedWidth.HasValue) { if (threedFormat.HasValue) { // This method can handle 0 being passed in for the requested height return GetFixedSwScaleFilter(threedFormat, requestedWidth.Value, 0); } - else - { - var widthParam = requestedWidth.Value.ToString(CultureInfo.InvariantCulture); - return string.Format( - CultureInfo.InvariantCulture, - "scale={0}:trunc(ow/a/2)*2", - widthParam); - } + var widthParam = requestedWidth.Value.ToString(CultureInfo.InvariantCulture); + + return string.Format( + CultureInfo.InvariantCulture, + "scale={0}:trunc(ow/a/2)*2", + widthParam); } // If a fixed height was requested - else if (requestedHeight.HasValue) + if (requestedHeight.HasValue) { var heightParam = requestedHeight.Value.ToString(CultureInfo.InvariantCulture); return string.Format( - CultureInfo.InvariantCulture, - "scale=trunc(oh*a/{1})*{1}:{0}", - heightParam, - scaleVal); + CultureInfo.InvariantCulture, + "scale=trunc(oh*a/{1})*{1}:{0}", + heightParam, + scaleVal); } // If a max width was requested - else if (requestedMaxWidth.HasValue) + if (requestedMaxWidth.HasValue) { var maxWidthParam = requestedMaxWidth.Value.ToString(CultureInfo.InvariantCulture); return string.Format( - CultureInfo.InvariantCulture, - "scale=trunc(min(max(iw\\,ih*a)\\,{0})/{1})*{1}:trunc(ow/a/2)*2", - maxWidthParam, - scaleVal); + CultureInfo.InvariantCulture, + "scale=trunc(min(max(iw\\,ih*a)\\,{0})/{1})*{1}:trunc(ow/a/2)*2", + maxWidthParam, + scaleVal); } // If a max height was requested - else if (requestedMaxHeight.HasValue) + if (requestedMaxHeight.HasValue) { var maxHeightParam = requestedMaxHeight.Value.ToString(CultureInfo.InvariantCulture); return string.Format( - CultureInfo.InvariantCulture, - "scale=trunc(oh*a/{1})*{1}:min(max(iw/a\\,ih)\\,{0})", - maxHeightParam, - scaleVal); + CultureInfo.InvariantCulture, + "scale=trunc(oh*a/{1})*{1}:min(max(iw/a\\,ih)\\,{0})", + maxHeightParam, + scaleVal); } return string.Empty; @@ -2739,7 +2883,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // default - if (filter == null) + if (filter is null) { if (requestedHeight > 0) { @@ -2774,22 +2918,32 @@ namespace MediaBrowser.Controller.MediaEncoding "yadif_cuda={0}:-1:0", doubleRateDeint ? "1" : "0"); } - else if (hwDeintSuffix.Contains("vaapi", StringComparison.OrdinalIgnoreCase)) + + if (hwDeintSuffix.Contains("vaapi", StringComparison.OrdinalIgnoreCase)) { return string.Format( CultureInfo.InvariantCulture, "deinterlace_vaapi=rate={0}", doubleRateDeint ? "field" : "frame"); } - else if (hwDeintSuffix.Contains("qsv", StringComparison.OrdinalIgnoreCase)) + + if (hwDeintSuffix.Contains("qsv", StringComparison.OrdinalIgnoreCase)) { return "deinterlace_qsv=mode=2"; } + if (hwDeintSuffix.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase)) + { + return string.Format( + CultureInfo.InvariantCulture, + "yadif_videotoolbox={0}:-1:0", + doubleRateDeint ? "1" : "0"); + } + return string.Empty; } - public static string GetHwTonemapFilter(EncodingOptions options, string hwTonemapSuffix, string videoFormat) + public string GetHwTonemapFilter(EncodingOptions options, string hwTonemapSuffix, string videoFormat) { if (string.IsNullOrEmpty(hwTonemapSuffix)) { @@ -2801,7 +2955,8 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(hwTonemapSuffix, "vaapi", StringComparison.OrdinalIgnoreCase)) { - args = "tonemap_vaapi=format={0}:p=bt709:t=bt709:m=bt709,procamp_vaapi=b={1}:c={2}:extra_hw_frames=16"; + args = "procamp_vaapi=b={2}:c={3}," + args + ":extra_hw_frames=32"; + return string.Format( CultureInfo.InvariantCulture, args, @@ -2809,7 +2964,8 @@ namespace MediaBrowser.Controller.MediaEncoding options.VppTonemappingBrightness, options.VppTonemappingContrast); } - else if (string.Equals(hwTonemapSuffix, "vulkan", StringComparison.OrdinalIgnoreCase)) + + if (string.Equals(hwTonemapSuffix, "vulkan", StringComparison.OrdinalIgnoreCase)) { args = "libplacebo=format={1}:tonemapping={2}:color_primaries=bt709:color_trc=bt709:colorspace=bt709:peak_detect=0:upscaler=none:downscaler=none"; @@ -2822,7 +2978,6 @@ namespace MediaBrowser.Controller.MediaEncoding { algorithm = "bt.2390"; } - else if (string.Equals(options.TonemappingAlgorithm, "none", StringComparison.OrdinalIgnoreCase)) { algorithm = "clip"; @@ -2832,14 +2987,24 @@ namespace MediaBrowser.Controller.MediaEncoding { args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709:tonemap={2}:peak={3}:desat={4}"; + if (string.Equals(options.TonemappingMode, "max", StringComparison.OrdinalIgnoreCase) + || string.Equals(options.TonemappingMode, "rgb", StringComparison.OrdinalIgnoreCase)) + { + if (_mediaEncoder.EncoderVersion >= _minFFmpegOclCuTonemapMode) + { + args += ":tonemap_mode={5}"; + } + } + if (options.TonemappingParam != 0) { - args += ":param={5}"; + args += ":param={6}"; } - if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(options.TonemappingRange, "tv", StringComparison.OrdinalIgnoreCase) + || string.Equals(options.TonemappingRange, "pc", StringComparison.OrdinalIgnoreCase)) { - args += ":range={6}"; + args += ":range={7}"; } } @@ -2851,10 +3016,80 @@ namespace MediaBrowser.Controller.MediaEncoding algorithm, options.TonemappingPeak, options.TonemappingDesat, + options.TonemappingMode, options.TonemappingParam, options.TonemappingRange); } + public string GetLibplaceboFilter( + EncodingOptions options, + string videoFormat, + bool doTonemap, + int? videoWidth, + int? videoHeight, + int? requestedWidth, + int? requestedHeight, + int? requestedMaxWidth, + int? requestedMaxHeight) + { + var (outWidth, outHeight) = GetFixedOutputSize( + videoWidth, + videoHeight, + requestedWidth, + requestedHeight, + requestedMaxWidth, + requestedMaxHeight); + + var isFormatFixed = !string.IsNullOrEmpty(videoFormat); + var isSizeFixed = !videoWidth.HasValue + || outWidth.Value != videoWidth.Value + || !videoHeight.HasValue + || outHeight.Value != videoHeight.Value; + + var sizeArg = isSizeFixed ? (":w=" + outWidth.Value + ":h=" + outHeight.Value) : string.Empty; + var formatArg = isFormatFixed ? (":format=" + videoFormat) : string.Empty; + var tonemapArg = string.Empty; + + if (doTonemap) + { + var algorithm = options.TonemappingAlgorithm; + var mode = options.TonemappingMode; + var range = options.TonemappingRange; + + if (string.Equals(algorithm, "bt2390", StringComparison.OrdinalIgnoreCase)) + { + algorithm = "bt.2390"; + } + else if (string.Equals(algorithm, "none", StringComparison.OrdinalIgnoreCase)) + { + algorithm = "clip"; + } + + tonemapArg = ":tonemapping=" + algorithm; + + if (string.Equals(mode, "max", StringComparison.OrdinalIgnoreCase) + || string.Equals(mode, "rgb", StringComparison.OrdinalIgnoreCase)) + { + tonemapArg += ":tonemapping_mode=" + mode; + } + + tonemapArg += ":peak_detect=0:color_primaries=bt709:color_trc=bt709:colorspace=bt709"; + + if (string.Equals(range, "tv", StringComparison.OrdinalIgnoreCase) + || string.Equals(range, "pc", StringComparison.OrdinalIgnoreCase)) + { + tonemapArg += ":range=" + range; + } + } + + return string.Format( + CultureInfo.InvariantCulture, + "libplacebo=upscaler=none:downscaler=none{0}{1}{2}", + sizeArg, + formatArg, + tonemapArg); + } + /// <summary> /// Gets the parameter of software filter chain. /// </summary> @@ -2884,7 +3119,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); var doDeintH2645 = doDeintH264 || doDeintHevc; - var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; @@ -2931,8 +3166,8 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (hasGraphicalSubs) { - // [0:s]scale=expr - var subSwScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + // [0:s]scale=s=1280x720 + var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0"); } @@ -2999,7 +3234,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doDeintH2645 = doDeintH264 || doDeintHevc; var doCuTonemap = IsHwTonemapAvailable(state, options); - var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -3118,9 +3353,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subSwScaleFilter = isSwDecoder - ? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH) - : GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0"); } @@ -3189,7 +3422,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doDeintH2645 = doDeintH264 || doDeintHevc; var doOclTonemap = IsHwTonemapAvailable(state, options); - var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -3257,7 +3490,7 @@ namespace MediaBrowser.Controller.MediaEncoding // OUTPUT nv12 surface(memory) // prefer hwmap to hwdownload on opencl. - var hwTransferFilter = hasGraphicalSubs ? "hwdownload" : "hwmap"; + var hwTransferFilter = hasGraphicalSubs ? "hwdownload" : "hwmap=mode=read"; mainFilters.Add(hwTransferFilter); mainFilters.Add("format=nv12"); } @@ -3320,9 +3553,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subSwScaleFilter = isSwDecoder - ? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH) - : GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0"); } @@ -3411,7 +3642,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doDeintH2645 = doDeintH264 || doDeintHevc; var doOclTonemap = IsHwTonemapAvailable(state, options); - var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -3502,7 +3733,7 @@ namespace MediaBrowser.Controller.MediaEncoding // OUTPUT nv12 surface(memory) // prefer hwmap to hwdownload on opencl. // qsv hwmap is not fully implemented for the time being. - mainFilters.Add(isHwmapUsable ? "hwmap" : "hwdownload"); + mainFilters.Add(isHwmapUsable ? "hwmap=mode=read" : "hwdownload"); mainFilters.Add("format=nv12"); } @@ -3574,9 +3805,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subSwScaleFilter = isSwDecoder - ? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH) - : GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0"); } @@ -3614,7 +3843,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doTonemap = doVaVppTonemap || doOclTonemap; var doDeintH2645 = doDeintH264 || doDeintHevc; - var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -3662,6 +3891,13 @@ namespace MediaBrowser.Controller.MediaEncoding var outFormat = doTonemap ? string.Empty : "nv12"; var hwScaleFilter = GetHwScaleFilter(isVaapiDecoder ? "vaapi" : "qsv", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + + // allocate extra pool sizes for vaapi vpp + if (!string.IsNullOrEmpty(hwScaleFilter) && isVaapiDecoder) + { + hwScaleFilter += ":extra_hw_frames=24"; + } + // hw scale mainFilters.Add(hwScaleFilter); } @@ -3708,7 +3944,7 @@ namespace MediaBrowser.Controller.MediaEncoding // OUTPUT nv12 surface(memory) // prefer hwmap to hwdownload on opencl/vaapi. // qsv hwmap is not fully implemented for the time being. - mainFilters.Add(isHwmapUsable ? "hwmap" : "hwdownload"); + mainFilters.Add(isHwmapUsable ? "hwmap=mode=read" : "hwdownload"); mainFilters.Add("format=nv12"); } @@ -3785,9 +4021,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subSwScaleFilter = isSwDecoder - ? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH) - : GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0"); } @@ -3891,7 +4125,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doTonemap = doVaVppTonemap || doOclTonemap; var doDeintH2645 = doDeintH264 || doDeintHevc; - var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -3939,6 +4173,13 @@ namespace MediaBrowser.Controller.MediaEncoding var outFormat = doTonemap ? string.Empty : "nv12"; var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + + // allocate extra pool sizes for vaapi vpp + if (!string.IsNullOrEmpty(hwScaleFilter)) + { + hwScaleFilter += ":extra_hw_frames=24"; + } + // hw scale mainFilters.Add(hwScaleFilter); } @@ -3980,7 +4221,7 @@ namespace MediaBrowser.Controller.MediaEncoding // OUTPUT nv12 surface(memory) // prefer hwmap to hwdownload on opencl/vaapi. - mainFilters.Add(isHwmapNotUsable ? "hwdownload" : "hwmap"); + mainFilters.Add(isHwmapNotUsable ? "hwdownload" : "hwmap=mode=read"); mainFilters.Add("format=nv12"); } @@ -4046,9 +4287,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subSwScaleFilter = isSwDecoder - ? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH) - : GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0"); @@ -4080,14 +4319,13 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isVaapiEncoder; - var isVaInVaOut = isVaapiDecoder && isVaapiEncoder; var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); var doVkTonemap = IsVulkanHwTonemapAvailable(state, options); var doDeintH2645 = doDeintH264 || doDeintHevc; - var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -4109,95 +4347,81 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add(swDeintFilter); } - var outFormat = doVkTonemap ? "yuv420p10le" : "nv12"; - var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); - // sw scale - mainFilters.Add(swScaleFilter); - mainFilters.Add("format=" + outFormat); - - // keep video at memory except vk tonemap, - // since the overhead caused by hwupload >>> using sw filter. - // sw => hw - if (doVkTonemap) + if (doVkTonemap || hasSubs) + { + // sw => hw + mainFilters.Add("hwupload=derive_device=vulkan"); + mainFilters.Add("format=vulkan"); + } + else { - mainFilters.Add("hwupload=derive_device=vulkan:extra_hw_frames=16"); + // sw scale + var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + mainFilters.Add(swScaleFilter); + mainFilters.Add("format=nv12"); } } else if (isVaapiDecoder) { // INPUT vaapi surface(vram) - // hw deint - if (doDeintH2645) + if (doVkTonemap || hasSubs) { - var deintFilter = GetHwDeinterlaceFilter(state, options, "vaapi"); - mainFilters.Add(deintFilter); + // map from vaapi to vulkan/drm via interop (Vega/gfx9+). + mainFilters.Add("hwmap=derive_device=vulkan"); + mainFilters.Add("format=vulkan"); } - - var outFormat = doVkTonemap ? string.Empty : (hasSubs && isVaInVaOut ? "bgra" : "nv12"); - var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH); - - // allocate extra pool sizes for overlay_vulkan - if (!string.IsNullOrEmpty(hwScaleFilter) && isVaInVaOut && hasSubs) + else { - hwScaleFilter += ":extra_hw_frames=32"; - } - - // hw scale - mainFilters.Add(hwScaleFilter); - } + // hw deint + if (doDeintH2645) + { + var deintFilter = GetHwDeinterlaceFilter(state, options, "vaapi"); + mainFilters.Add(deintFilter); + } - if ((isVaapiDecoder && doVkTonemap) || (isVaInVaOut && (doVkTonemap || hasSubs))) - { - // map from vaapi to vulkan via vaapi-vulkan interop (Vega/gfx9+). - mainFilters.Add("hwmap=derive_device=vulkan"); + // hw scale + var hwScaleFilter = GetHwScaleFilter("vaapi", "nv12", inW, inH, reqW, reqH, reqMaxW, reqMaxH); + mainFilters.Add(hwScaleFilter); + } } - // vk tonemap - if (doVkTonemap) + // vk libplacebo + if (doVkTonemap || hasSubs) { - var outFormat = isVaInVaOut && hasSubs ? "bgra" : "nv12"; - var tonemapFilter = GetHwTonemapFilter(options, "vulkan", outFormat); - mainFilters.Add(tonemapFilter); + var libplaceboFilter = GetLibplaceboFilter(options, "bgra", doVkTonemap, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + mainFilters.Add(libplaceboFilter); } - if (doVkTonemap && isVaInVaOut && !hasSubs) + if (doVkTonemap && !hasSubs) { - // OUTPUT vaapi(nv12/bgra) surface(vram) - // reverse-mapping via vaapi-vulkan interop. - mainFilters.Add("hwmap=derive_device=vaapi:reverse=1"); + // OUTPUT vaapi(nv12) surface(vram) + // map from vulkan/drm to vaapi via interop (Vega/gfx9+). + mainFilters.Add("hwmap=derive_device=drm"); + mainFilters.Add("format=drm_prime"); + mainFilters.Add("hwmap=derive_device=vaapi"); mainFilters.Add("format=vaapi"); - } - var memoryOutput = false; - var isUploadForVkTonemap = isSwDecoder && doVkTonemap; - if ((isVaapiDecoder && isSwEncoder) || isUploadForVkTonemap) - { - memoryOutput = true; - - // OUTPUT nv12 surface(memory) - mainFilters.Add("hwdownload"); - mainFilters.Add("format=nv12"); - } + // clear the surf->meta_offset and output nv12 + mainFilters.Add("scale_vaapi=format=nv12"); - // OUTPUT nv12 surface(memory) - if (isSwDecoder && isVaapiEncoder) - { - memoryOutput = true; + // hw deint + if (doDeintH2645) + { + var deintFilter = GetHwDeinterlaceFilter(state, options, "vaapi"); + mainFilters.Add(deintFilter); + } } - if (memoryOutput) + if (!hasSubs) { - // text subtitles - if (hasTextSubs) + // OUTPUT nv12 surface(memory) + if (isSwEncoder && (doVkTonemap || isVaapiDecoder)) { - var textSubtitlesFilter = GetTextSubtitlesFilter(state, false, false); - mainFilters.Add(textSubtitlesFilter); + mainFilters.Add("hwdownload"); + mainFilters.Add("format=nv12"); } - } - if (memoryOutput && isVaapiEncoder) - { - if (!hasGraphicalSubs) + if (isSwDecoder && isVaapiEncoder && !doVkTonemap) { mainFilters.Add("hwupload_vaapi"); } @@ -4206,52 +4430,53 @@ namespace MediaBrowser.Controller.MediaEncoding /* Make sub and overlay filters for subtitle stream */ var subFilters = new List<string>(); var overlayFilters = new List<string>(); - if (isVaInVaOut) + if (hasSubs) { - if (hasSubs) + if (hasGraphicalSubs) { - if (hasGraphicalSubs) - { - // scale=s=1280x720,format=bgra,hwupload - var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); - subFilters.Add(subSwScaleFilter); - subFilters.Add("format=bgra"); - } - else if (hasTextSubs) - { - var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, hasAssSubs ? 10 : 5); - var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true); - subFilters.Add(alphaSrcFilter); - subFilters.Add("format=bgra"); - subFilters.Add(subTextSubtitlesFilter); - } - - subFilters.Add("hwupload=derive_device=vulkan:extra_hw_frames=16"); + // scale=s=1280x720,format=bgra,hwupload + var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + subFilters.Add(subSwScaleFilter); + subFilters.Add("format=bgra"); + } + else if (hasTextSubs) + { + var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, hasAssSubs ? 10 : 5); + var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true); + subFilters.Add(alphaSrcFilter); + subFilters.Add("format=bgra"); + subFilters.Add(subTextSubtitlesFilter); + } - overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0"); + subFilters.Add("hwupload=derive_device=vulkan"); + subFilters.Add("format=vulkan"); - // explicitly sync using libplacebo. - overlayFilters.Add("libplacebo=format=nv12:upscaler=none:downscaler=none"); + overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0"); - // OUTPUT vaapi(nv12/bgra) surface(vram) - // reverse-mapping via vaapi-vulkan interop. - overlayFilters.Add("hwmap=derive_device=vaapi:reverse=1"); - overlayFilters.Add("format=vaapi"); + if (isSwEncoder) + { + // OUTPUT nv12 surface(memory) + overlayFilters.Add("scale_vulkan=format=nv12"); + overlayFilters.Add("hwdownload"); + overlayFilters.Add("format=nv12"); } - } - else if (memoryOutput) - { - if (hasGraphicalSubs) + else if (isVaapiEncoder) { - var subSwScaleFilter = isSwDecoder - ? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH) - : GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); - subFilters.Add(subSwScaleFilter); - overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0"); + // OUTPUT vaapi(nv12) surface(vram) + // map from vulkan/drm to vaapi via interop (Vega/gfx9+). + overlayFilters.Add("hwmap=derive_device=drm"); + overlayFilters.Add("format=drm_prime"); + overlayFilters.Add("hwmap=derive_device=vaapi"); + overlayFilters.Add("format=vaapi"); - if (isVaapiEncoder) + // clear the surf->meta_offset and output nv12 + overlayFilters.Add("scale_vaapi=format=nv12"); + + // hw deint + if (doDeintH2645) { - overlayFilters.Add("hwupload_vaapi"); + var deintFilter = GetHwDeinterlaceFilter(state, options, "vaapi"); + overlayFilters.Add(deintFilter); } } } @@ -4286,7 +4511,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doDeintH2645 = doDeintH264 || doDeintHevc; var doOclTonemap = IsHwTonemapAvailable(state, options); - var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; @@ -4332,6 +4557,13 @@ namespace MediaBrowser.Controller.MediaEncoding outFormat = doOclTonemap ? string.Empty : "nv12"; var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + + // allocate extra pool sizes for vaapi vpp + if (!string.IsNullOrEmpty(hwScaleFilter)) + { + hwScaleFilter += ":extra_hw_frames=24"; + } + // hw scale mainFilters.Add(hwScaleFilter); } @@ -4420,9 +4652,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subSwScaleFilter = isSwDecoder - ? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH) - : GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0"); @@ -4437,6 +4667,75 @@ namespace MediaBrowser.Controller.MediaEncoding } /// <summary> + /// Gets the parameter of Apple VideoToolBox filter chain. + /// </summary> + /// <param name="state">Encoding state.</param> + /// <param name="options">Encoding options.</param> + /// <param name="vidEncoder">Video encoder to use.</param> + /// <returns>The tuple contains three lists: main, sub and overlay filters.</returns> + public (List<string> MainFilters, List<string> SubFilters, List<string> OverlayFilters) GetAppleVidFilterChain( + EncodingJobInfo state, + EncodingOptions options, + string vidEncoder) + { + if (!string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) + { + return (null, null, null); + } + + var swFilterChain = GetSwVidFilterChain(state, options, vidEncoder); + + if (!options.EnableHardwareEncoding) + { + return swFilterChain; + } + + if (_mediaEncoder.EncoderVersion.CompareTo(new Version("5.0.0")) < 0) + { + // All features used here requires ffmpeg 5.0 or later, fallback to software filters if using an old ffmpeg + return swFilterChain; + } + + var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); + var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); + var doDeintH2645 = doDeintH264 || doDeintHevc; + var inW = state.VideoStream?.Width; + var inH = state.VideoStream?.Height; + var reqW = state.BaseRequest.Width; + var reqH = state.BaseRequest.Height; + var reqMaxW = state.BaseRequest.MaxWidth; + var reqMaxH = state.BaseRequest.MaxHeight; + var threeDFormat = state.MediaSource.Video3DFormat; + var newfilters = new List<string>(); + var noOverlay = swFilterChain.OverlayFilters.Count == 0; + var supportsHwDeint = _mediaEncoder.SupportsFilter("yadif_videotoolbox"); + // fallback to software filters if we are using filters not supported by hardware yet. + var useHardwareFilters = noOverlay && (!doDeintH2645 || supportsHwDeint); + + if (!useHardwareFilters) + { + return swFilterChain; + } + + // ffmpeg cannot use videotoolbox to scale + var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + newfilters.Add(swScaleFilter); + + // hwupload on videotoolbox encoders can automatically convert AVFrame into its CVPixelBuffer equivalent + // videotoolbox will automatically convert the CVPixelBuffer to a pixel format the encoder supports, so we don't have to set a pixel format explicitly here + // This will reduce CPU usage significantly on UHD videos with 10 bit colors because we bypassed the ffmpeg pixel format conversion + newfilters.Add("hwupload"); + + if (doDeintH2645) + { + var deintFilter = GetHwDeinterlaceFilter(state, options, "videotoolbox"); + newfilters.Add(deintFilter); + } + + return (newfilters, swFilterChain.SubFilters, swFilterChain.OverlayFilters); + } + + /// <summary> /// Gets the parameter of video processing filters. /// </summary> /// <param name="state">Encoding state.</param> @@ -4449,12 +4748,12 @@ namespace MediaBrowser.Controller.MediaEncoding string outputVideoCodec) { var videoStream = state.VideoStream; - if (videoStream == null) + if (videoStream is null) { return string.Empty; } - var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; @@ -4478,6 +4777,10 @@ namespace MediaBrowser.Controller.MediaEncoding { (mainFilters, subFilters, overlayFilters) = GetAmdVidFilterChain(state, options, outputVideoCodec); } + else if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) + { + (mainFilters, subFilters, overlayFilters) = GetAppleVidFilterChain(state, options, outputVideoCodec); + } else { (mainFilters, subFilters, overlayFilters) = GetSwVidFilterChain(state, options, outputVideoCodec); @@ -4504,7 +4807,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (overlayFilters?.Count > 0 && subFilters?.Count > 0 - && state.SubtitleStream != null) + && state.SubtitleStream is not null) { // overlay graphical/text subtitles var subStr = string.Format( @@ -4591,32 +4894,33 @@ namespace MediaBrowser.Controller.MediaEncoding public static int GetVideoColorBitDepth(EncodingJobInfo state) { var videoStream = state.VideoStream; - if (videoStream != null) + if (videoStream is not null) { if (videoStream.BitDepth.HasValue) { return videoStream.BitDepth.Value; } - else if (string.Equals(videoStream.PixelFormat, "yuv420p", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoStream.PixelFormat, "yuvj420p", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoStream.PixelFormat, "yuv444p", StringComparison.OrdinalIgnoreCase)) + + if (string.Equals(videoStream.PixelFormat, "yuv420p", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuvj420p", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv444p", StringComparison.OrdinalIgnoreCase)) { return 8; } - else if (string.Equals(videoStream.PixelFormat, "yuv420p10le", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoStream.PixelFormat, "yuv444p10le", StringComparison.OrdinalIgnoreCase)) + + if (string.Equals(videoStream.PixelFormat, "yuv420p10le", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv444p10le", StringComparison.OrdinalIgnoreCase)) { return 10; } - else if (string.Equals(videoStream.PixelFormat, "yuv420p12le", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoStream.PixelFormat, "yuv444p12le", StringComparison.OrdinalIgnoreCase)) + + if (string.Equals(videoStream.PixelFormat, "yuv420p12le", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv444p12le", StringComparison.OrdinalIgnoreCase)) { return 12; } - else - { - return 8; - } + + return 8; } return 0; @@ -4632,13 +4936,13 @@ namespace MediaBrowser.Controller.MediaEncoding { var videoStream = state.VideoStream; var mediaSource = state.MediaSource; - if (videoStream == null || mediaSource == null) + if (videoStream is null || mediaSource is null) { return null; } // HWA decoders can handle both video files and video folders. - var videoType = mediaSource.VideoType; + var videoType = state.VideoType; if (videoType != VideoType.VideoFile && videoType != VideoType.Iso && videoType != VideoType.Dvd @@ -4779,8 +5083,18 @@ namespace MediaBrowser.Controller.MediaEncoding var isVideotoolboxSupported = isMacOS && _mediaEncoder.SupportsHwaccel("videotoolbox"); var isCodecAvailable = options.HardwareDecodingCodecs.Contains(videoCodec, StringComparison.OrdinalIgnoreCase); + var ffmpegVersion = _mediaEncoder.EncoderVersion; + // Set the av1 codec explicitly to trigger hw accelerator, otherwise libdav1d will be used. - var isAv1 = string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase); + var isAv1 = ffmpegVersion < _minFFmpegImplictHwaccel + && string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase); + + // Allow profile mismatch if decoding H.264 baseline with d3d11va and vaapi hwaccels. + var profileMismatch = string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) + && string.Equals(state.VideoStream?.Profile, "baseline", StringComparison.OrdinalIgnoreCase); + + // Disable the extra internal copy in nvdec. We already handle it in filter chain. + var nvdecNoInternalCopy = ffmpegVersion >= _minFFmpegHwaUnsafeOutput; if (bitDepth == 10 && isCodecAvailable) { @@ -4806,14 +5120,16 @@ namespace MediaBrowser.Controller.MediaEncoding { if (isVaapiSupported && isCodecAvailable) { - return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty); + return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty) + + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty); } if (isD3d11Supported && isCodecAvailable) { // set -threads 3 to intel d3d11va decoder explicitly. Lower threads may result in dead lock. // on newer devices such as Xe, the larger the init_pool_size, the longer the initialization time for opencl to derive from d3d11. - return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + " -threads 3" + (isAv1 ? " -c:v av1" : string.Empty); + return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + " -threads 3" + (isAv1 ? " -c:v av1" : string.Empty); } } else @@ -4833,13 +5149,12 @@ namespace MediaBrowser.Controller.MediaEncoding if (options.EnableEnhancedNvdecDecoder) { // set -threads 1 to nvdec decoder explicitly since it doesn't implement threading support. - return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty) + " -threads 1" + (isAv1 ? " -c:v av1" : string.Empty); - } - else - { - // cuvid decoder doesn't have threading issue. - return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty); + return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty) + + (nvdecNoInternalCopy ? " -hwaccel_flags +unsafe_output" : string.Empty) + " -threads 1" + (isAv1 ? " -c:v av1" : string.Empty); } + + // cuvid decoder doesn't have threading issue. + return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty); } } @@ -4848,7 +5163,8 @@ namespace MediaBrowser.Controller.MediaEncoding { if (isD3d11Supported && isCodecAvailable) { - return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty); + return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty); } } @@ -4857,9 +5173,11 @@ namespace MediaBrowser.Controller.MediaEncoding && isVaapiSupported && isCodecAvailable) { - return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty); + return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty) + + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty); } + // Apple videotoolbox if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase) && isVideotoolboxSupported && isCodecAvailable) @@ -5194,7 +5512,8 @@ namespace MediaBrowser.Controller.MediaEncoding // Automatically set thread count return mustSetThreadCount ? Math.Max(Environment.ProcessorCount - 1, 1) : 0; } - else if (threads >= Environment.ProcessorCount) + + if (threads >= Environment.ProcessorCount) { return Environment.ProcessorCount; } @@ -5205,7 +5524,7 @@ namespace MediaBrowser.Controller.MediaEncoding #nullable disable public void TryStreamCopy(EncodingJobInfo state) { - if (state.VideoStream != null && CanStreamCopyVideo(state, state.VideoStream)) + if (state.VideoStream is not null && CanStreamCopyVideo(state, state.VideoStream)) { state.OutputVideoCodec = "copy"; } @@ -5214,13 +5533,13 @@ 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.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)) + if (user is not null && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)) { state.OutputVideoCodec = "copy"; } } - if (state.AudioStream != null + if (state.AudioStream is not null && CanStreamCopyAudio(state, state.AudioStream, state.SupportedAudioCodecs)) { state.OutputAudioCodec = "copy"; @@ -5230,7 +5549,7 @@ 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.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) + if (user is not null && !user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) { state.OutputAudioCodec = "copy"; } @@ -5427,7 +5746,7 @@ namespace MediaBrowser.Controller.MediaEncoding state.SubtitleDeliveryMethod = videoRequest.SubtitleMethod; state.AudioStream = GetMediaStream(mediaStreams, videoRequest.AudioStreamIndex, MediaStreamType.Audio); - if (state.SubtitleStream != null && !state.SubtitleStream.IsExternal) + if (state.SubtitleStream is not null && !state.SubtitleStream.IsExternal) { state.InternalSubtitleStreamOffset = mediaStreams.Where(i => i.Type == MediaStreamType.Subtitle && !i.IsExternal).ToList().IndexOf(state.SubtitleStream); } @@ -5445,7 +5764,7 @@ namespace MediaBrowser.Controller.MediaEncoding var request = state.BaseRequest; var supportedAudioCodecs = state.SupportedAudioCodecs; - if (request != null && supportedAudioCodecs != null && supportedAudioCodecs.Length > 0) + if (request is not null && supportedAudioCodecs is not null && supportedAudioCodecs.Length > 0) { var supportedAudioCodecsList = supportedAudioCodecs.ToList(); @@ -5458,7 +5777,7 @@ namespace MediaBrowser.Controller.MediaEncoding } var supportedVideoCodecs = state.SupportedVideoCodecs; - if (request != null && supportedVideoCodecs != null && supportedVideoCodecs.Length > 0) + if (request is not null && supportedVideoCodecs is not null && supportedVideoCodecs.Length > 0) { var supportedVideoCodecsList = supportedVideoCodecs.ToList(); @@ -5478,15 +5797,23 @@ namespace MediaBrowser.Controller.MediaEncoding return; } - var inputChannels = audioStream == null ? 6 : audioStream.Channels ?? 6; + var inputChannels = audioStream is null ? 6 : audioStream.Channels ?? 6; + var shiftAudioCodecs = new List<string>(); if (inputChannels >= 6) { - return; + // DTS and TrueHD are not supported by HLS + // Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used + shiftAudioCodecs.Add("dca"); + shiftAudioCodecs.Add("truehd"); + } + else + { + // Transcoding to 2ch ac3 or eac3 almost always causes a playback failure + // Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used + shiftAudioCodecs.Add("ac3"); + shiftAudioCodecs.Add("eac3"); } - // Transcoding to 2ch ac3 almost always causes a playback failure - // Keep it in the supported codecs list, but shift it to the end of the list so that if transcoding happens, another codec is used - var shiftAudioCodecs = new[] { "ac3", "eac3" }; if (audioCodecs.All(i => shiftAudioCodecs.Contains(i, StringComparison.OrdinalIgnoreCase))) { return; @@ -5530,7 +5857,7 @@ namespace MediaBrowser.Controller.MediaEncoding private void NormalizeSubtitleEmbed(EncodingJobInfo state) { - if (state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Embed) + if (state.SubtitleStream is null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Embed) { return; } @@ -5545,7 +5872,7 @@ namespace MediaBrowser.Controller.MediaEncoding public string GetSubtitleEmbedArguments(EncodingJobInfo state) { - if (state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Embed) + if (state.SubtitleStream is null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Embed) { return string.Empty; } @@ -5577,7 +5904,7 @@ namespace MediaBrowser.Controller.MediaEncoding && 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"; + format = " -f mp4 -movflags frag_keyframe+empty_moov+delay_moov"; } var threads = GetNumberOfThreads(state, encodingOptions, videoCodec); @@ -5626,7 +5953,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (IsCopyCodec(videoCodec)) { - if (state.VideoStream != null + if (state.VideoStream is not null && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) { @@ -5656,14 +5983,16 @@ namespace MediaBrowser.Controller.MediaEncoding args += keyFrameArg; - var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasGraphicalSubs = state.SubtitleStream is not null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasCopyTs = false; // video processing filters. var videoProcessParam = GetVideoProcessingFilterParam(state, encodingOptions, videoCodec); - args += videoProcessParam; + var negativeMapArgs = GetNegativeMapArgsByFilters(state, videoProcessParam); + + args = negativeMapArgs + args + videoProcessParam; hasCopyTs = videoProcessParam.Contains("copyts", StringComparison.OrdinalIgnoreCase); @@ -5676,7 +6005,7 @@ namespace MediaBrowser.Controller.MediaEncoding args += " -avoid_negative_ts disabled"; - if (!(state.SubtitleStream != null && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)) + if (!(state.SubtitleStream is not null && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)) { args += " -start_at_zero"; } @@ -5703,7 +6032,7 @@ namespace MediaBrowser.Controller.MediaEncoding public string GetProgressiveVideoAudioArguments(EncodingJobInfo state, EncodingOptions encodingOptions) { // If the video doesn't have an audio stream, return a default. - if (state.AudioStream == null && state.VideoStream != null) + if (state.AudioStream is null && state.VideoStream is not null) { return string.Empty; } @@ -5718,19 +6047,25 @@ namespace MediaBrowser.Controller.MediaEncoding return args; } - // Add the number of audio channels var channels = state.OutputAudioChannels; - if (channels.HasValue) + if (channels.HasValue && ((channels.Value != 2 && state.AudioStream.Channels <= 5) || encodingOptions.DownMixStereoAlgorithm == DownMixStereoAlgorithms.None)) { args += " -ac " + channels.Value; } var bitrate = state.OutputAudioBitrate; - - if (bitrate.HasValue) + if (bitrate.HasValue && !LosslessAudioCodecs.Contains(codec, StringComparison.OrdinalIgnoreCase)) { - args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture); + var vbrParam = GetAudioVbrModeParam(codec, bitrate.Value / (channels ?? 2)); + if (encodingOptions.EnableAudioVbr && vbrParam is not null) + { + args += vbrParam; + } + else + { + args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture); + } } if (state.OutputAudioSampleRate.HasValue) @@ -5748,18 +6083,33 @@ namespace MediaBrowser.Controller.MediaEncoding var audioTranscodeParams = new List<string>(); var bitrate = state.OutputAudioBitrate; + var channels = state.OutputAudioChannels; + var outputCodec = state.OutputAudioCodec; - if (bitrate.HasValue) + if (bitrate.HasValue && !LosslessAudioCodecs.Contains(outputCodec, StringComparison.OrdinalIgnoreCase)) { - audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture)); + var vbrParam = GetAudioVbrModeParam(GetAudioEncoder(state), bitrate.Value / (channels ?? 2)); + if (encodingOptions.EnableAudioVbr && vbrParam is not null) + { + audioTranscodeParams.Add(vbrParam); + } + else + { + audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture)); + } } - if (state.OutputAudioChannels.HasValue) + if (channels.HasValue) { audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture)); } - if (!string.Equals(state.OutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(outputCodec)) + { + audioTranscodeParams.Add("-acodec " + GetAudioEncoder(state)); + } + + if (!string.Equals(outputCodec, "opus", StringComparison.OrdinalIgnoreCase)) { // opus only supports specific sampling rates var sampleRate = state.OutputAudioSampleRate; @@ -5778,6 +6128,13 @@ namespace MediaBrowser.Controller.MediaEncoding } } + // Copy the movflags from GetProgressiveVideoFullCommandLine + // See #9248 and the associated PR for why this is needed + if (_mp4ContainerNames.Contains(state.OutputContainer)) + { + audioTranscodeParams.Add("-movflags empty_moov+delay_moov"); + } + var threads = GetNumberOfThreads(state, encodingOptions, null); var inputModifier = GetInputModifier(state, encodingOptions, null); diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index c9625cf1da..a6b5416601 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -40,7 +40,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (!_transcodeReasons.HasValue) { - if (BaseRequest.TranscodeReasons == null) + if (BaseRequest.TranscodeReasons is null) { _transcodeReasons = 0; return 0; @@ -147,7 +147,7 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (VideoStream != null && VideoStream.Width.HasValue && VideoStream.Height.HasValue) + if (VideoStream is not null && VideoStream.Width.HasValue && VideoStream.Height.HasValue) { var size = new ImageDimensions(VideoStream.Width.Value, VideoStream.Height.Value); @@ -174,7 +174,7 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (VideoStream != null && VideoStream.Width.HasValue && VideoStream.Height.HasValue) + if (VideoStream is not null && VideoStream.Width.HasValue && VideoStream.Height.HasValue) { var size = new ImageDimensions(VideoStream.Width.Value, VideoStream.Height.Value); @@ -204,7 +204,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputAudioCodec)) { - if (AudioStream != null) + if (AudioStream is not null) { return AudioStream.SampleRate; } @@ -227,7 +227,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputAudioCodec)) { - if (AudioStream != null) + if (AudioStream is not null) { return AudioStream.BitDepth; } @@ -250,8 +250,7 @@ namespace MediaBrowser.Controller.MediaEncoding } var level = GetRequestedLevel(ActualOutputVideoCodec); - if (!string.IsNullOrEmpty(level) - && double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (double.TryParse(level, CultureInfo.InvariantCulture, out var result)) { return result; } @@ -305,7 +304,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { - return VideoStream == null ? null : (VideoStream.AverageFrameRate ?? VideoStream.RealFrameRate); + return VideoStream is null ? null : (VideoStream.AverageFrameRate ?? VideoStream.RealFrameRate); } return BaseRequest.MaxFramerate ?? BaseRequest.Framerate; @@ -419,7 +418,7 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (VideoStream == null) + if (VideoStream is null) { return null; } @@ -437,7 +436,7 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (AudioStream == null) + if (AudioStream is null) { return null; } @@ -556,7 +555,7 @@ namespace MediaBrowser.Controller.MediaEncoding public bool DeInterlace(string videoCodec, bool forceDeinterlaceIfSourceIsInterlaced) { var videoStream = VideoStream; - var isInputInterlaced = videoStream != null && videoStream.IsInterlaced; + var isInputInterlaced = videoStream is not null && videoStream.IsInterlaced; if (!isInputInterlaced) { @@ -645,8 +644,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(codec)) { var value = BaseRequest.GetOption(codec, "maxrefframes"); - if (!string.IsNullOrEmpty(value) - && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } @@ -665,8 +663,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(codec)) { var value = BaseRequest.GetOption(codec, "videobitdepth"); - if (!string.IsNullOrEmpty(value) - && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } @@ -685,8 +682,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(codec)) { var value = BaseRequest.GetOption(codec, "audiobitdepth"); - if (!string.IsNullOrEmpty(value) - && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } @@ -700,8 +696,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(codec)) { var value = BaseRequest.GetOption(codec, "audiochannels"); - if (!string.IsNullOrEmpty(value) - && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index 52c57b906e..f830b9f298 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -38,7 +38,7 @@ namespace MediaBrowser.Controller.MediaEncoding Version EncoderVersion { get; } /// <summary> - /// Whether p key pausing is supported. + /// Gets a value indicating whether p key pausing is supported. /// </summary> /// <value><c>true</c> if p key pausing is supported, <c>false</c> otherwise.</value> bool IsPkeyPauseSupported { get; } @@ -154,6 +154,14 @@ namespace MediaBrowser.Controller.MediaEncoding string GetInputArgument(string inputFile, MediaSourceInfo mediaSource); /// <summary> + /// Gets the input argument. + /// </summary> + /// <param name="inputFiles">The input files.</param> + /// <param name="mediaSource">The mediaSource.</param> + /// <returns>System.String.</returns> + string GetInputArgument(IReadOnlyList<string> inputFiles, MediaSourceInfo mediaSource); + + /// <summary> /// Gets the input argument for an external subtitle file. /// </summary> /// <param name="inputFile">The input file.</param> @@ -194,6 +202,20 @@ namespace MediaBrowser.Controller.MediaEncoding /// <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); + IReadOnlyList<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber); + + /// <summary> + /// Gets the primary playlist of .m2ts files. + /// </summary> + /// <param name="path">The to the .m2ts files.</param> + /// <returns>A playlist.</returns> + IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path); + + /// <summary> + /// Generates a FFmpeg concat config for the source. + /// </summary> + /// <param name="source">The <see cref="MediaSourceInfo"/>.</param> + /// <param name="concatFilePath">The path the config should be written to.</param> + void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath); } } diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index d8475f12ae..3b34af4e96 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -86,7 +86,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var rate = parts[i + 1]; - if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val)) + if (float.TryParse(rate, CultureInfo.InvariantCulture, out var val)) { framerate = val; } @@ -95,7 +95,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var rate = part.Split('=', 2)[^1]; - if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val)) + if (float.TryParse(rate, CultureInfo.InvariantCulture, out var val)) { framerate = val; } @@ -127,7 +127,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (scale.HasValue) { - if (long.TryParse(size, NumberStyles.Any, CultureInfo.InvariantCulture, out var val)) + if (long.TryParse(size, CultureInfo.InvariantCulture, out var val)) { bytesTranscoded = val * scale.Value; } @@ -146,7 +146,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (scale.HasValue) { - if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val)) + if (float.TryParse(rate, CultureInfo.InvariantCulture, out var val)) { bitRate = (int)Math.Ceiling(val * scale.Value); } diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index 647de50030..0524999c79 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -166,7 +166,7 @@ namespace MediaBrowser.Controller.Net var data = await GetDataToSend().ConfigureAwait(false); - if (data != null) + if (data is not null) { await connection.SendAsync( new WebSocketMessage<TReturnDataType> @@ -204,7 +204,7 @@ namespace MediaBrowser.Controller.Net { var connection = _activeConnections.FirstOrDefault(c => c.Item1 == message.Connection); - if (connection != null) + if (connection is not null) { DisposeConnection(connection); } @@ -227,9 +227,15 @@ namespace MediaBrowser.Controller.Net connection.Item2.Cancel(); connection.Item2.Dispose(); } - catch (ObjectDisposedException) + catch (ObjectDisposedException ex) { // TODO Investigate and properly fix. + Logger.LogError(ex, "Object Disposed"); + } + catch (Exception ex) + { + // TODO Investigate and properly fix. + Logger.LogError(ex, "Error disposing websocket"); } lock (_activeConnections) diff --git a/MediaBrowser.Controller/Net/WebSocketMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessage.cs new file mode 100644 index 0000000000..c02bcd70b6 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessage.cs @@ -0,0 +1,28 @@ +using System; +using System.Text.Json.Serialization; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net; + +/// <summary> +/// Websocket message without data. +/// </summary> +public abstract class WebSocketMessage +{ + /// <summary> + /// Gets or sets the type of the message. + /// TODO make this abstract and get only. + /// </summary> + public virtual SessionMessageType MessageType { get; set; } + + /// <summary> + /// Gets or sets the message id. + /// </summary> + public Guid MessageId { get; set; } + + /// <summary> + /// Gets or sets the server id. + /// </summary> + [JsonIgnore] + public string? ServerId { get; set; } +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessageOfT.cs b/MediaBrowser.Controller/Net/WebSocketMessageOfT.cs new file mode 100644 index 0000000000..7c35c8010d --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessageOfT.cs @@ -0,0 +1,33 @@ +#pragma warning disable SA1649 // File name must equal class name. + +namespace MediaBrowser.Controller.Net; + +/// <summary> +/// Class WebSocketMessage. +/// </summary> +/// <typeparam name="T">The type of the data.</typeparam> +// TODO make this abstract, remove empty ctor. +public class WebSocketMessage<T> : WebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="WebSocketMessage{T}"/> class. + /// </summary> + public WebSocketMessage() + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="WebSocketMessage{T}"/> class. + /// </summary> + /// <param name="data">The data to send.</param> + protected WebSocketMessage(T data) + { + Data = data; + } + + /// <summary> + /// Gets or sets the data. + /// </summary> + // TODO make this set only. + public T? Data { get; set; } +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/IInboundWebSocketMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/IInboundWebSocketMessage.cs new file mode 100644 index 0000000000..c3cf9955ad --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/IInboundWebSocketMessage.cs @@ -0,0 +1,10 @@ +#pragma warning disable CA1040 + +namespace MediaBrowser.Controller.Net.WebSocketMessages; + +/// <summary> +/// Interface representing that the websocket message is inbound. +/// </summary> +public interface IInboundWebSocketMessage +{ +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/IOutboundWebSocketMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/IOutboundWebSocketMessage.cs new file mode 100644 index 0000000000..c74a254a68 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/IOutboundWebSocketMessage.cs @@ -0,0 +1,10 @@ +#pragma warning disable CA1040 + +namespace MediaBrowser.Controller.Net.WebSocketMessages; + +/// <summary> +/// Interface representing that the websocket message is outbound. +/// </summary> +public interface IOutboundWebSocketMessage +{ +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStartMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStartMessage.cs new file mode 100644 index 0000000000..b9f71b9225 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStartMessage.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.ComponentModel; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; + +/// <summary> +/// Activity log entry start message. +/// </summary> +public class ActivityLogEntryStartMessage : WebSocketMessage<IReadOnlyCollection<ActivityLogEntry>>, IInboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="ActivityLogEntryStartMessage"/> class. + /// </summary> + /// <param name="data">Collection of activity log entries.</param> + public ActivityLogEntryStartMessage(IReadOnlyCollection<ActivityLogEntry> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.ActivityLogEntryStart)] + public override SessionMessageType MessageType => SessionMessageType.ActivityLogEntryStart; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStopMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStopMessage.cs new file mode 100644 index 0000000000..eac129b20a --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStopMessage.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.ComponentModel; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; + +/// <summary> +/// Activity log entry stop message. +/// </summary> +public class ActivityLogEntryStopMessage : WebSocketMessage<IReadOnlyCollection<ActivityLogEntry>>, IInboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="ActivityLogEntryStopMessage"/> class. + /// </summary> + /// <param name="data">Collection of activity log entries.</param> + public ActivityLogEntryStopMessage(IReadOnlyCollection<ActivityLogEntry> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.ActivityLogEntryStop)] + public override SessionMessageType MessageType => SessionMessageType.ActivityLogEntryStop; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStartMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStartMessage.cs new file mode 100644 index 0000000000..dd2a7145e3 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStartMessage.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.Tasks; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; + +/// <summary> +/// Scheduled tasks info start message. +/// </summary> +public class ScheduledTasksInfoStartMessage : WebSocketMessage<IReadOnlyCollection<TaskInfo>>, IInboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="ScheduledTasksInfoStartMessage"/> class. + /// </summary> + /// <param name="data">Collection of task info.</param> + public ScheduledTasksInfoStartMessage(IReadOnlyCollection<TaskInfo> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.ScheduledTasksInfoStart)] + public override SessionMessageType MessageType => SessionMessageType.ScheduledTasksInfoStart; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStopMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStopMessage.cs new file mode 100644 index 0000000000..84e1f01667 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStopMessage.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.Tasks; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; + +/// <summary> +/// Scheduled tasks info stop message. +/// </summary> +public class ScheduledTasksInfoStopMessage : WebSocketMessage<IReadOnlyCollection<TaskInfo>>, IInboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="ScheduledTasksInfoStopMessage"/> class. + /// </summary> + /// <param name="data">Collection of task info.</param> + public ScheduledTasksInfoStopMessage(IReadOnlyCollection<TaskInfo> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.ScheduledTasksInfoStop)] + public override SessionMessageType MessageType => SessionMessageType.ScheduledTasksInfoStop; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStartMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStartMessage.cs new file mode 100644 index 0000000000..e35a5dc3ad --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStartMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; + +/// <summary> +/// Sessions start message. +/// </summary> +public class SessionsStartMessage : WebSocketMessage<SessionInfo>, IInboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="SessionsStartMessage"/> class. + /// </summary> + /// <param name="data">Session info.</param> + public SessionsStartMessage(SessionInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SessionsStart)] + public override SessionMessageType MessageType => SessionMessageType.SessionsStart; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStopMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStopMessage.cs new file mode 100644 index 0000000000..7e3582d640 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStopMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; + +/// <summary> +/// Sessions stop message. +/// </summary> +public class SessionsStopMessage : WebSocketMessage<SessionInfo>, IInboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="SessionsStopMessage"/> class. + /// </summary> + /// <param name="data">Session info.</param> + public SessionsStopMessage(SessionInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SessionsStop)] + public override SessionMessageType MessageType => SessionMessageType.SessionsStop; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessage.cs new file mode 100644 index 0000000000..20ca888e11 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessage.cs @@ -0,0 +1,9 @@ +namespace MediaBrowser.Controller.Net.WebSocketMessages; + +/// <summary> +/// Class representing the list of outbound websocket message types. +/// Only used in openapi generation. +/// </summary> +public class InboundWebSocketMessage : WebSocketMessage +{ +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ActivityLogEntryMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ActivityLogEntryMessage.cs new file mode 100644 index 0000000000..5650ee4bbe --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ActivityLogEntryMessage.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.ComponentModel; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Activity log created message. +/// </summary> +public class ActivityLogEntryMessage : WebSocketMessage<IReadOnlyList<ActivityLogEntry>>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="ActivityLogEntryMessage"/> class. + /// </summary> + /// <param name="data">List of activity log entries.</param> + public ActivityLogEntryMessage(IReadOnlyList<ActivityLogEntry> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.ActivityLogEntry)] + public override SessionMessageType MessageType => SessionMessageType.ActivityLogEntry; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ForceKeepAliveMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ForceKeepAliveMessage.cs new file mode 100644 index 0000000000..94ade5e817 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ForceKeepAliveMessage.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Force keep alive websocket messages. +/// </summary> +public class ForceKeepAliveMessage : WebSocketMessage<int>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="ForceKeepAliveMessage"/> class. + /// </summary> + /// <param name="data">The timeout in seconds.</param> + public ForceKeepAliveMessage(int data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.ForceKeepAlive)] + public override SessionMessageType MessageType => SessionMessageType.ForceKeepAlive; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/GeneralCommandMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/GeneralCommandMessage.cs new file mode 100644 index 0000000000..6c71e73f9d --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/GeneralCommandMessage.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// General command websocket message. +/// </summary> +public class GeneralCommandMessage : WebSocketMessage<GeneralCommand>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="GeneralCommandMessage"/> class. + /// </summary> + /// <param name="data">The general command.</param> + public GeneralCommandMessage(GeneralCommand data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.GeneralCommand)] + public override SessionMessageType MessageType => SessionMessageType.GeneralCommand; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/LibraryChangedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/LibraryChangedMessage.cs new file mode 100644 index 0000000000..6432ae8efb --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/LibraryChangedMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Library changed message. +/// </summary> +public class LibraryChangedMessage : WebSocketMessage<LibraryUpdateInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="LibraryChangedMessage"/> class. + /// </summary> + /// <param name="data">The library update info.</param> + public LibraryChangedMessage(LibraryUpdateInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.LibraryChanged)] + public override SessionMessageType MessageType => SessionMessageType.LibraryChanged; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlayMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlayMessage.cs new file mode 100644 index 0000000000..7f943bda10 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlayMessage.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Play command websocket message. +/// </summary> +public class PlayMessage : WebSocketMessage<PlayRequest>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="PlayMessage"/> class. + /// </summary> + /// <param name="data">The play request.</param> + public PlayMessage(PlayRequest data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.Play)] + public override SessionMessageType MessageType => SessionMessageType.Play; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlaystateMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlaystateMessage.cs new file mode 100644 index 0000000000..804ccb37d6 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlaystateMessage.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Playstate message. +/// </summary> +public class PlaystateMessage : WebSocketMessage<PlaystateRequest>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="PlaystateMessage"/> class. + /// </summary> + /// <param name="data">Playstate request data.</param> + public PlaystateMessage(PlaystateRequest data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.Playstate)] + public override SessionMessageType MessageType => SessionMessageType.Playstate; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCancelledMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCancelledMessage.cs new file mode 100644 index 0000000000..3d7dc5c937 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCancelledMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.Updates; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Plugin installation cancelled message. +/// </summary> +public class PluginInstallationCancelledMessage : WebSocketMessage<InstallationInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="PluginInstallationCancelledMessage"/> class. + /// </summary> + /// <param name="data">Installation info.</param> + public PluginInstallationCancelledMessage(InstallationInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.PackageInstallationCancelled)] + public override SessionMessageType MessageType => SessionMessageType.PackageInstallationCancelled; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCompletedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCompletedMessage.cs new file mode 100644 index 0000000000..81268007fd --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCompletedMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.Updates; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Plugin installation completed message. +/// </summary> +public class PluginInstallationCompletedMessage : WebSocketMessage<InstallationInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="PluginInstallationCompletedMessage"/> class. + /// </summary> + /// <param name="data">Installation info.</param> + public PluginInstallationCompletedMessage(InstallationInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.PackageInstallationCompleted)] + public override SessionMessageType MessageType => SessionMessageType.PackageInstallationCompleted; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationFailedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationFailedMessage.cs new file mode 100644 index 0000000000..9177f12938 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationFailedMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.Updates; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Plugin installation failed message. +/// </summary> +public class PluginInstallationFailedMessage : WebSocketMessage<InstallationInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="PluginInstallationFailedMessage"/> class. + /// </summary> + /// <param name="data">Installation info.</param> + public PluginInstallationFailedMessage(InstallationInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.PackageInstallationFailed)] + public override SessionMessageType MessageType => SessionMessageType.PackageInstallationFailed; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallingMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallingMessage.cs new file mode 100644 index 0000000000..e371440a0f --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallingMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.Updates; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Package installing message. +/// </summary> +public class PluginInstallingMessage : WebSocketMessage<InstallationInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="PluginInstallingMessage"/> class. + /// </summary> + /// <param name="data">Installation info.</param> + public PluginInstallingMessage(InstallationInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.PackageInstalling)] + public override SessionMessageType MessageType => SessionMessageType.PackageInstalling; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginUninstalledMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginUninstalledMessage.cs new file mode 100644 index 0000000000..b2994fc956 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginUninstalledMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Plugin uninstalled message. +/// </summary> +public class PluginUninstalledMessage : WebSocketMessage<PluginInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="PluginUninstalledMessage"/> class. + /// </summary> + /// <param name="data">Plugin info.</param> + public PluginUninstalledMessage(PluginInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.PackageUninstalled)] + public override SessionMessageType MessageType => SessionMessageType.PackageUninstalled; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RefreshProgressMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RefreshProgressMessage.cs new file mode 100644 index 0000000000..42dbc30295 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RefreshProgressMessage.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Refresh progress message. +/// </summary> +public class RefreshProgressMessage : WebSocketMessage<Dictionary<string, string>>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="RefreshProgressMessage"/> class. + /// </summary> + /// <param name="data">Refresh progress data.</param> + public RefreshProgressMessage(Dictionary<string, string> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.RefreshProgress)] + public override SessionMessageType MessageType => SessionMessageType.RefreshProgress; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RestartRequiredMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RestartRequiredMessage.cs new file mode 100644 index 0000000000..3f3d9e4c84 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RestartRequiredMessage.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Restart required. +/// </summary> +public class RestartRequiredMessage : WebSocketMessage, IOutboundWebSocketMessage +{ + /// <inheritdoc /> + [DefaultValue(SessionMessageType.RestartRequired)] + public override SessionMessageType MessageType => SessionMessageType.RestartRequired; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTaskEndedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTaskEndedMessage.cs new file mode 100644 index 0000000000..d69662b004 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTaskEndedMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.Tasks; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Scheduled task ended message. +/// </summary> +public class ScheduledTaskEndedMessage : WebSocketMessage<TaskResult>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="ScheduledTaskEndedMessage"/> class. + /// </summary> + /// <param name="data">Task result.</param> + public ScheduledTaskEndedMessage(TaskResult data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.ScheduledTaskEnded)] + public override SessionMessageType MessageType => SessionMessageType.ScheduledTaskEnded; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTasksInfoMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTasksInfoMessage.cs new file mode 100644 index 0000000000..41a05b0de2 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTasksInfoMessage.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.Tasks; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Scheduled tasks info message. +/// </summary> +public class ScheduledTasksInfoMessage : WebSocketMessage<IReadOnlyList<TaskInfo>>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="ScheduledTasksInfoMessage"/> class. + /// </summary> + /// <param name="data">List of task infos.</param> + public ScheduledTasksInfoMessage(IReadOnlyList<TaskInfo> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.ScheduledTasksInfo)] + public override SessionMessageType MessageType => SessionMessageType.ScheduledTasksInfo; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCancelledMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCancelledMessage.cs new file mode 100644 index 0000000000..d4950b8b67 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCancelledMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Series timer cancelled message. +/// </summary> +public class SeriesTimerCancelledMessage : WebSocketMessage<TimerEventInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="SeriesTimerCancelledMessage"/> class. + /// </summary> + /// <param name="data">The timer event info.</param> + public SeriesTimerCancelledMessage(TimerEventInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SeriesTimerCancelled)] + public override SessionMessageType MessageType => SessionMessageType.SeriesTimerCancelled; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCreatedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCreatedMessage.cs new file mode 100644 index 0000000000..091c10be6d --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCreatedMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Series timer created message. +/// </summary> +public class SeriesTimerCreatedMessage : WebSocketMessage<TimerEventInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="SeriesTimerCreatedMessage"/> class. + /// </summary> + /// <param name="data">timer event info.</param> + public SeriesTimerCreatedMessage(TimerEventInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SeriesTimerCreated)] + public override SessionMessageType MessageType => SessionMessageType.SeriesTimerCreated; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerRestartingMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerRestartingMessage.cs new file mode 100644 index 0000000000..a465d8b008 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerRestartingMessage.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Server restarting down message. +/// </summary> +public class ServerRestartingMessage : WebSocketMessage, IOutboundWebSocketMessage +{ + /// <inheritdoc /> + [DefaultValue(SessionMessageType.ServerRestarting)] + public override SessionMessageType MessageType => SessionMessageType.ServerRestarting; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerShuttingDownMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerShuttingDownMessage.cs new file mode 100644 index 0000000000..0b998a5239 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerShuttingDownMessage.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Server shutting down message. +/// </summary> +public class ServerShuttingDownMessage : WebSocketMessage, IOutboundWebSocketMessage +{ + /// <inheritdoc /> + [DefaultValue(SessionMessageType.ServerShuttingDown)] + public override SessionMessageType MessageType => SessionMessageType.ServerShuttingDown; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs new file mode 100644 index 0000000000..4c91e0bca2 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Sessions message. +/// </summary> +public class SessionsMessage : WebSocketMessage<SessionInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="SessionsMessage"/> class. + /// </summary> + /// <param name="data">Session info.</param> + public SessionsMessage(SessionInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.Sessions)] + public override SessionMessageType MessageType => SessionMessageType.Sessions; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayCommandMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayCommandMessage.cs new file mode 100644 index 0000000000..17a0fc66e5 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayCommandMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Sync play command. +/// </summary> +public class SyncPlayCommandMessage : WebSocketMessage<SendCommand>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="SyncPlayCommandMessage"/> class. + /// </summary> + /// <param name="data">The send command.</param> + public SyncPlayCommandMessage(SendCommand data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SyncPlayCommand)] + public override SessionMessageType MessageType => SessionMessageType.SyncPlayCommand; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs new file mode 100644 index 0000000000..d145d0e01a --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Untyped sync play command. +/// </summary> +public class SyncPlayGroupUpdateCommandMessage : WebSocketMessage<GroupUpdate>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandMessage"/> class. + /// </summary> + /// <param name="data">The send command.</param> + public SyncPlayGroupUpdateCommandMessage(GroupUpdate data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] + public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs new file mode 100644 index 0000000000..668392c66b --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs @@ -0,0 +1,25 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Sync play group update command with group info. +/// GroupUpdateTypes: GroupJoined. +/// </summary> +public class SyncPlayGroupUpdateCommandOfGroupInfoMessage : WebSocketMessage<GroupUpdate<GroupInfoDto>>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfGroupInfoMessage"/> class. + /// </summary> + /// <param name="data">The group info.</param> + public SyncPlayGroupUpdateCommandOfGroupInfoMessage(GroupUpdate<GroupInfoDto> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] + public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs new file mode 100644 index 0000000000..ec8c3344f4 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs @@ -0,0 +1,25 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Sync play group update command with group state update. +/// GroupUpdateTypes: StateUpdate. +/// </summary> +public class SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage : WebSocketMessage<GroupUpdate<GroupStateUpdate>>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage"/> class. + /// </summary> + /// <param name="data">The group info.</param> + public SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage(GroupUpdate<GroupStateUpdate> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] + public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs new file mode 100644 index 0000000000..465363f143 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs @@ -0,0 +1,25 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Sync play group update command with play queue update. +/// GroupUpdateTypes: PlayQueue. +/// </summary> +public class SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage : WebSocketMessage<GroupUpdate<PlayQueueUpdate>>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage"/> class. + /// </summary> + /// <param name="data">The play queue update.</param> + public SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage(GroupUpdate<PlayQueueUpdate> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] + public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs new file mode 100644 index 0000000000..b87e9bf715 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs @@ -0,0 +1,25 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Sync play group update command with string. +/// GroupUpdateTypes: GroupDoesNotExist (error), LibraryAccessDenied (error), NotInGroup (error), GroupLeft (groupId), UserJoined (username), UserLeft (username). +/// </summary> +public class SyncPlayGroupUpdateCommandOfStringMessage : WebSocketMessage<GroupUpdate<string>>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfStringMessage"/> class. + /// </summary> + /// <param name="data">The send command.</param> + public SyncPlayGroupUpdateCommandOfStringMessage(GroupUpdate<string> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] + public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCancelledMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCancelledMessage.cs new file mode 100644 index 0000000000..0e70549ef8 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCancelledMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Timer cancelled message. +/// </summary> +public class TimerCancelledMessage : WebSocketMessage<TimerEventInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="TimerCancelledMessage"/> class. + /// </summary> + /// <param name="data">Timer event info.</param> + public TimerCancelledMessage(TimerEventInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.TimerCancelled)] + public override SessionMessageType MessageType => SessionMessageType.TimerCancelled; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCreatedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCreatedMessage.cs new file mode 100644 index 0000000000..295b3081ce --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCreatedMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Timer created message. +/// </summary> +public class TimerCreatedMessage : WebSocketMessage<TimerEventInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="TimerCreatedMessage"/> class. + /// </summary> + /// <param name="data">Timer event info.</param> + public TimerCreatedMessage(TimerEventInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.TimerCreated)] + public override SessionMessageType MessageType => SessionMessageType.TimerCreated; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDataChangedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDataChangedMessage.cs new file mode 100644 index 0000000000..b60769540d --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDataChangedMessage.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// User data changed message. +/// </summary> +public class UserDataChangedMessage : WebSocketMessage<UserDataChangeInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="UserDataChangedMessage"/> class. + /// </summary> + /// <param name="data">The data change info.</param> + public UserDataChangedMessage(UserDataChangeInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.UserDataChanged)] + public override SessionMessageType MessageType => SessionMessageType.UserDataChanged; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDeletedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDeletedMessage.cs new file mode 100644 index 0000000000..6d527be7f2 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDeletedMessage.cs @@ -0,0 +1,24 @@ +using System; +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// User deleted message. +/// </summary> +public class UserDeletedMessage : WebSocketMessage<Guid>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="UserDeletedMessage"/> class. + /// </summary> + /// <param name="data">The user id.</param> + public UserDeletedMessage(Guid data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.UserDeleted)] + public override SessionMessageType MessageType => SessionMessageType.UserDeleted; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserUpdatedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserUpdatedMessage.cs new file mode 100644 index 0000000000..99e9a1f911 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserUpdatedMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// User updated message. +/// </summary> +public class UserUpdatedMessage : WebSocketMessage<UserDto>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="UserUpdatedMessage"/> class. + /// </summary> + /// <param name="data">The user dto.</param> + public UserUpdatedMessage(UserDto data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.UserUpdated)] + public override SessionMessageType MessageType => SessionMessageType.UserUpdated; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs new file mode 100644 index 0000000000..dba3c8392b --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs @@ -0,0 +1,9 @@ +namespace MediaBrowser.Controller.Net.WebSocketMessages; + +/// <summary> +/// Class representing the list of outbound websocket message types. +/// Only used in openapi generation. +/// </summary> +public class OutboundWebSocketMessage : WebSocketMessage +{ +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Shared/KeepAliveMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Shared/KeepAliveMessage.cs new file mode 100644 index 0000000000..7f636212ca --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Shared/KeepAliveMessage.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Shared; + +/// <summary> +/// Keep alive websocket messages. +/// </summary> +public class KeepAliveMessage : WebSocketMessage<int>, IInboundWebSocketMessage, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="KeepAliveMessage"/> class. + /// </summary> + /// <param name="data">The seconds to keep alive for.</param> + public KeepAliveMessage(int data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.KeepAlive)] + public override SessionMessageType MessageType => SessionMessageType.KeepAlive; +} diff --git a/MediaBrowser.Controller/Notifications/INotificationManager.cs b/MediaBrowser.Controller/Notifications/INotificationManager.cs deleted file mode 100644 index 7caba1097a..0000000000 --- a/MediaBrowser.Controller/Notifications/INotificationManager.cs +++ /dev/null @@ -1,43 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Notifications; - -namespace MediaBrowser.Controller.Notifications -{ - public interface INotificationManager - { - /// <summary> - /// Sends the notification. - /// </summary> - /// <param name="request">The request.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - Task SendNotification(NotificationRequest request, CancellationToken cancellationToken); - - Task SendNotification(NotificationRequest request, BaseItem? relatedItem, CancellationToken cancellationToken); - - /// <summary> - /// Adds the parts. - /// </summary> - /// <param name="services">The services.</param> - /// <param name="notificationTypeFactories">The notification type factories.</param> - void AddParts(IEnumerable<INotificationService> services, IEnumerable<INotificationTypeFactory> notificationTypeFactories); - - /// <summary> - /// Gets the notification types. - /// </summary> - /// <returns>IEnumerable{NotificationTypeInfo}.</returns> - List<NotificationTypeInfo> GetNotificationTypes(); - - /// <summary> - /// Gets the notification services. - /// </summary> - /// <returns>IEnumerable{NotificationServiceInfo}.</returns> - IEnumerable<NameIdPair> GetNotificationServices(); - } -} diff --git a/MediaBrowser.Controller/Notifications/INotificationService.cs b/MediaBrowser.Controller/Notifications/INotificationService.cs deleted file mode 100644 index 535c08795b..0000000000 --- a/MediaBrowser.Controller/Notifications/INotificationService.cs +++ /dev/null @@ -1,34 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Data.Entities; - -namespace MediaBrowser.Controller.Notifications -{ - public interface INotificationService - { - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - string Name { get; } - - /// <summary> - /// Sends the notification. - /// </summary> - /// <param name="request">The request.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - Task SendNotification(UserNotification request, CancellationToken cancellationToken); - - /// <summary> - /// Determines whether [is enabled for user] [the specified user identifier]. - /// </summary> - /// <param name="user">The user.</param> - /// <returns><c>true</c> if [is enabled for user] [the specified user identifier]; otherwise, <c>false</c>.</returns> - bool IsEnabledForUser(User user); - } -} diff --git a/MediaBrowser.Controller/Notifications/INotificationTypeFactory.cs b/MediaBrowser.Controller/Notifications/INotificationTypeFactory.cs deleted file mode 100644 index 52a3e120b5..0000000000 --- a/MediaBrowser.Controller/Notifications/INotificationTypeFactory.cs +++ /dev/null @@ -1,16 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using MediaBrowser.Model.Notifications; - -namespace MediaBrowser.Controller.Notifications -{ - public interface INotificationTypeFactory - { - /// <summary> - /// Gets the notification types. - /// </summary> - /// <returns>IEnumerable{NotificationTypeInfo}.</returns> - IEnumerable<NotificationTypeInfo> GetNotificationTypes(); - } -} diff --git a/MediaBrowser.Controller/Notifications/UserNotification.cs b/MediaBrowser.Controller/Notifications/UserNotification.cs deleted file mode 100644 index 4be0e09ae7..0000000000 --- a/MediaBrowser.Controller/Notifications/UserNotification.cs +++ /dev/null @@ -1,25 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using Jellyfin.Data.Entities; -using MediaBrowser.Model.Notifications; - -namespace MediaBrowser.Controller.Notifications -{ - public class UserNotification - { - public string Name { get; set; } - - public string Description { get; set; } - - public string Url { get; set; } - - public NotificationLevel Level { get; set; } - - public DateTime Date { get; set; } - - public User User { get; set; } - } -} diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 24f7b5cd36..2c52b2b45e 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -28,7 +28,7 @@ namespace MediaBrowser.Controller.Persistence /// </summary> /// <param name="items">The items.</param> /// <param name="cancellationToken">The cancellation token.</param> - void SaveItems(IEnumerable<BaseItem> items, CancellationToken cancellationToken); + void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken); void SaveImages(BaseItem item); diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index f6c5920709..d1a51c2cf6 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -56,5 +56,19 @@ namespace MediaBrowser.Controller.Playlists /// <param name="newIndex">The new index.</param> /// <returns>Task.</returns> Task MoveItemAsync(string playlistId, string entryId, int newIndex); + + /// <summary> + /// Removed all playlists of a user. + /// If the playlist is shared, ownership is transferred. + /// </summary> + /// <param name="userId">The user id.</param> + /// <returns>Task.</returns> + Task RemovePlaylistsAsync(Guid userId); + + /// <summary> + /// Saves a playlist. + /// </summary> + /// <param name="item">The playlist.</param> + void SavePlaylistFile(Playlist item); } } diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index 7ae9ff7463..498df5ab06 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -15,6 +15,7 @@ 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; namespace MediaBrowser.Controller.Playlists @@ -33,10 +34,13 @@ namespace MediaBrowser.Controller.Playlists public Playlist() { Shares = Array.Empty<Share>(); + OpenAccess = false; } public Guid OwnerUserId { get; set; } + public bool OpenAccess { get; set; } + public Share[] Shares { get; set; } [JsonIgnore] @@ -165,7 +169,7 @@ namespace MediaBrowser.Controller.Playlists public static List<BaseItem> GetPlaylistItems(string playlistMediaType, IEnumerable<BaseItem> inputItems, User user, DtoOptions options) { - if (user != null) + if (user is not null) { inputItems = inputItems.Where(i => i.IsVisible(user)); } @@ -232,7 +236,13 @@ namespace MediaBrowser.Controller.Playlists return base.IsVisible(user); } - if (user.Id.Equals(OwnerUserId)) + if (OpenAccess) + { + return true; + } + + var userId = user.Id; + if (userId.Equals(OwnerUserId)) { return true; } @@ -240,10 +250,9 @@ namespace MediaBrowser.Controller.Playlists var shares = Shares; if (shares.Length == 0) { - return base.IsVisible(user); + return false; } - var userId = user.Id; return shares.Any(share => Guid.TryParse(share.UserId, out var id) && id.Equals(userId)); } diff --git a/MediaBrowser.Controller/Providers/EpisodeInfo.cs b/MediaBrowser.Controller/Providers/EpisodeInfo.cs index b59a037384..c4ad352a3b 100644 --- a/MediaBrowser.Controller/Providers/EpisodeInfo.cs +++ b/MediaBrowser.Controller/Providers/EpisodeInfo.cs @@ -12,10 +12,13 @@ namespace MediaBrowser.Controller.Providers public EpisodeInfo() { SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + SeasonProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } public Dictionary<string, string> SeriesProviderIds { get; set; } + public Dictionary<string, string> SeasonProviderIds { get; set; } + public int? IndexNumberEnd { get; set; } public bool IsMissingEpisode { get; set; } diff --git a/MediaBrowser.Controller/Providers/IHasOrder.cs b/MediaBrowser.Controller/Providers/IHasOrder.cs index 9fde0e6958..77b0407a20 100644 --- a/MediaBrowser.Controller/Providers/IHasOrder.cs +++ b/MediaBrowser.Controller/Providers/IHasOrder.cs @@ -1,9 +1,14 @@ -#pragma warning disable CS1591 - namespace MediaBrowser.Controller.Providers { + /// <summary> + /// Interface IHasOrder. + /// </summary> public interface IHasOrder { + /// <summary> + /// Gets the order. + /// </summary> + /// <value>The order.</value> int Order { get; } } } diff --git a/MediaBrowser.Controller/Providers/IMetadataService.cs b/MediaBrowser.Controller/Providers/IMetadataService.cs index 05fbb18ee4..f0f1d18624 100644 --- a/MediaBrowser.Controller/Providers/IMetadataService.cs +++ b/MediaBrowser.Controller/Providers/IMetadataService.cs @@ -23,6 +23,11 @@ namespace MediaBrowser.Controller.Providers /// <returns><c>true</c> if this instance can refresh the specified item.</returns> bool CanRefresh(BaseItem item); + /// <summary> + /// Determines whether this instance primarily targets the specified type. + /// </summary> + /// <param name="type">The type.</param> + /// <returns><c>true</c> if this instance primarily targets the specified type.</returns> bool CanRefreshPrimary(Type type); /// <summary> diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs index 44bc4a50cb..7e0a69586c 100644 --- a/MediaBrowser.Controller/Providers/IProviderManager.cs +++ b/MediaBrowser.Controller/Providers/IProviderManager.cs @@ -132,6 +132,24 @@ namespace MediaBrowser.Controller.Providers IEnumerable<ImageProviderInfo> GetRemoteImageProviderInfo(BaseItem item); /// <summary> + /// Gets the image providers for the provided item. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="refreshOptions">The image refresh options.</param> + /// <returns>The image providers for the item.</returns> + IEnumerable<IImageProvider> GetImageProviders(BaseItem item, ImageRefreshOptions refreshOptions); + + /// <summary> + /// Gets the metadata providers for the provided item. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="libraryOptions">The library options.</param> + /// <typeparam name="T">The type of metadata provider.</typeparam> + /// <returns>The metadata providers.</returns> + IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions) + where T : BaseItem; + + /// <summary> /// Gets all metadata plugins. /// </summary> /// <returns>IEnumerable{MetadataPlugin}.</returns> @@ -198,7 +216,7 @@ namespace MediaBrowser.Controller.Providers /// <returns>Task{HttpResponseInfo}.</returns> Task<HttpResponseMessage> GetSearchImage(string providerName, string url, CancellationToken cancellationToken); - Dictionary<Guid, Guid> GetRefreshQueue(); + HashSet<Guid> GetRefreshQueue(); void OnRefreshStart(BaseItem item); diff --git a/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs b/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs index f146decb60..9ad0c26b0c 100644 --- a/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs +++ b/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -8,20 +6,44 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Controller.Providers { + /// <summary> + /// Interface IRemoteMetadataProvider. + /// </summary> public interface IRemoteMetadataProvider : IMetadataProvider { } + /// <summary> + /// Interface IRemoteMetadataProvider. + /// </summary> + /// <typeparam name="TItemType">The type of <see cref="BaseItem" />.</typeparam> + /// <typeparam name="TLookupInfoType">The type of <see cref="ItemLookupInfo" />.</typeparam> public interface IRemoteMetadataProvider<TItemType, in TLookupInfoType> : IMetadataProvider<TItemType>, IRemoteMetadataProvider, IRemoteSearchProvider<TLookupInfoType> where TItemType : BaseItem, IHasLookupInfo<TLookupInfoType> where TLookupInfoType : ItemLookupInfo, new() { + /// <summary> + /// Gets the metadata for a specific LookupInfoType. + /// </summary> + /// <param name="info">The LookupInfoType to get metadata for.</param> + /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param> + /// <returns>A task returning a MetadataResult for the specific LookupInfoType.</returns> Task<MetadataResult<TItemType>> GetMetadata(TLookupInfoType info, CancellationToken cancellationToken); } + /// <summary> + /// Interface IRemoteMetadataProvider. + /// </summary> + /// <typeparam name="TLookupInfoType">The type of <see cref="ItemLookupInfo" />.</typeparam> public interface IRemoteSearchProvider<in TLookupInfoType> : IRemoteSearchProvider where TLookupInfoType : ItemLookupInfo { + /// <summary> + /// Gets the list of <see cref="RemoteSearchResult"/> for a specific LookupInfoType. + /// </summary> + /// <param name="searchInfo">The LookupInfoType to search for.</param> + /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param> + /// <returns>A task returning RemoteSearchResults for the searchInfo.</returns> Task<IEnumerable<RemoteSearchResult>> GetSearchResults(TLookupInfoType searchInfo, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs index fd73ed5f80..05b4d43a5a 100644 --- a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs +++ b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs @@ -1,6 +1,7 @@ #pragma warning disable CA1819, CS1591 using System; +using System.Collections.Generic; using System.Linq; using MediaBrowser.Model.Entities; @@ -23,7 +24,7 @@ namespace MediaBrowser.Controller.Providers public bool ReplaceAllImages { get; set; } - public ImageType[] ReplaceImages { get; set; } + public IReadOnlyList<ImageType> ReplaceImages { get; set; } public bool IsAutomated { get; set; } diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs index a38bbaf695..9e91a8bcd7 100644 --- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs +++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs @@ -26,13 +26,14 @@ namespace MediaBrowser.Controller.Providers ReplaceAllMetadata = copy.ReplaceAllMetadata; EnableRemoteContentProbe = copy.EnableRemoteContentProbe; + IsAutomated = copy.IsAutomated; ImageRefreshMode = copy.ImageRefreshMode; ReplaceAllImages = copy.ReplaceAllImages; ReplaceImages = copy.ReplaceImages; SearchResult = copy.SearchResult; RemoveOldMetadata = copy.RemoveOldMetadata; - if (copy.RefreshPaths != null && copy.RefreshPaths.Length > 0) + if (copy.RefreshPaths is not null && copy.RefreshPaths.Length > 0) { RefreshPaths ??= Array.Empty<string>(); @@ -58,7 +59,7 @@ namespace MediaBrowser.Controller.Providers public bool RefreshItem(BaseItem item) { - if (RefreshPaths != null && RefreshPaths.Length > 0) + if (RefreshPaths is not null && RefreshPaths.Length > 0) { return RefreshPaths.Contains(item.Path ?? string.Empty, StringComparison.OrdinalIgnoreCase); } diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs index 58a0fa2a9a..952dd48703 100644 --- a/MediaBrowser.Controller/Providers/MetadataResult.cs +++ b/MediaBrowser.Controller/Providers/MetadataResult.cs @@ -59,7 +59,7 @@ namespace MediaBrowser.Controller.Providers /// </summary> public void ResetPeople() { - if (People == null) + if (People is null) { People = new List<PersonInfo>(); } @@ -83,7 +83,7 @@ namespace MediaBrowser.Controller.Providers } } - if (userData == null) + if (userData is null) { userData = new UserItemData() { diff --git a/MediaBrowser.Controller/Resolvers/ItemResolver.cs b/MediaBrowser.Controller/Resolvers/ItemResolver.cs index e7bf013fa2..a6da8384e8 100644 --- a/MediaBrowser.Controller/Resolvers/ItemResolver.cs +++ b/MediaBrowser.Controller/Resolvers/ItemResolver.cs @@ -46,7 +46,7 @@ namespace MediaBrowser.Controller.Resolvers { var item = Resolve(args); - if (item != null) + if (item is not null) { SetInitialItemValues(item, args); } diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs index b38ee11462..c8b29aa1f3 100644 --- a/MediaBrowser.Controller/Session/ISessionController.cs +++ b/MediaBrowser.Controller/Session/ISessionController.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index b16399598c..0c4719a0e5 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -7,7 +7,6 @@ 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.Library; using MediaBrowser.Model.Session; @@ -58,16 +57,6 @@ namespace MediaBrowser.Controller.Session event EventHandler<SessionEventArgs> CapabilitiesChanged; /// <summary> - /// Occurs when [authentication failed]. - /// </summary> - event EventHandler<GenericEventArgs<AuthenticationRequest>> AuthenticationFailed; - - /// <summary> - /// Occurs when [authentication succeeded]. - /// </summary> - event EventHandler<GenericEventArgs<AuthenticationResult>> AuthenticationSucceeded; - - /// <summary> /// Gets the sessions. /// </summary> /// <value>The sessions.</value> diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index b4520ae48f..25bf23d61b 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -64,7 +64,7 @@ namespace MediaBrowser.Controller.Session { get { - if (Capabilities == null) + if (Capabilities is null) { return Array.Empty<string>(); } @@ -182,7 +182,7 @@ namespace MediaBrowser.Controller.Session { get { - if (Capabilities == null || !Capabilities.SupportsMediaControl) + if (Capabilities is null || !Capabilities.SupportsMediaControl) { return false; } @@ -204,7 +204,7 @@ namespace MediaBrowser.Controller.Session { get { - if (Capabilities == null || !Capabilities.SupportsMediaControl) + if (Capabilities is null || !Capabilities.SupportsMediaControl) { return false; } @@ -239,7 +239,7 @@ namespace MediaBrowser.Controller.Session /// </summary> /// <value>The supported commands.</value> public IReadOnlyList<GeneralCommandType> SupportedCommands - => Capabilities == null ? Array.Empty<GeneralCommandType>() : Capabilities.SupportedCommands; + => Capabilities is null ? Array.Empty<GeneralCommandType>() : Capabilities.SupportedCommands; public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory) { @@ -296,7 +296,7 @@ namespace MediaBrowser.Controller.Session { _lastProgressInfo = progressInfo; - if (_progressTimer == null) + if (_progressTimer is null) { _progressTimer = new Timer(OnProgressTimerCallback, null, 1000, 1000); } @@ -315,7 +315,7 @@ namespace MediaBrowser.Controller.Session } var progressInfo = _lastProgressInfo; - if (progressInfo == null) + if (progressInfo is null) { return; } @@ -357,7 +357,7 @@ namespace MediaBrowser.Controller.Session { lock (_progressLock) { - if (_progressTimer != null) + if (_progressTimer is not null) { _progressTimer.Dispose(); _progressTimer = null; diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs index 52aa44024d..fcfc18a644 100644 --- a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs +++ b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs @@ -1,9 +1,6 @@ -#nullable disable - #pragma warning disable CS1591 using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; @@ -20,12 +17,6 @@ namespace MediaBrowser.Controller.Subtitles event EventHandler<SubtitleDownloadFailureEventArgs> SubtitleDownloadFailure; /// <summary> - /// Adds the parts. - /// </summary> - /// <param name="subtitleProviders">The subtitle providers.</param> - void AddParts(IEnumerable<ISubtitleProvider> subtitleProviders); - - /// <summary> /// Searches the subtitles. /// </summary> /// <param name="video">The video.</param> diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs index 2164945560..dcc06db1ed 100644 --- a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs +++ b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs @@ -533,11 +533,9 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates _logger.LogWarning("Session {SessionId} is seeking to wrong position, correcting.", session.Id); return; } - else - { - // Session is ready. - context.SetBuffering(session, false); - } + + // Session is ready. + context.SetBuffering(session, false); if (!context.IsBuffering()) { diff --git a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs index 3a7685f347..c0a168192e 100644 --- a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs +++ b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs @@ -23,13 +23,13 @@ namespace MediaBrowser.Controller.SyncPlay.Queue /// The sorted playlist. /// </summary> /// <value>The sorted playlist, or play queue of the group.</value> - private List<QueueItem> _sortedPlaylist = new List<QueueItem>(); + private List<SyncPlayQueueItem> _sortedPlaylist = new List<SyncPlayQueueItem>(); /// <summary> /// The shuffled playlist. /// </summary> /// <value>The shuffled playlist, or play queue of the group.</value> - private List<QueueItem> _shuffledPlaylist = new List<QueueItem>(); + private List<SyncPlayQueueItem> _shuffledPlaylist = new List<SyncPlayQueueItem>(); /// <summary> /// Initializes a new instance of the <see cref="PlayQueueManager" /> class. @@ -76,7 +76,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue /// Gets the current playlist considering the shuffle mode. /// </summary> /// <returns>The playlist.</returns> - public IReadOnlyList<QueueItem> GetPlaylist() + public IReadOnlyList<SyncPlayQueueItem> GetPlaylist() { return GetPlaylistInternal(); } @@ -93,7 +93,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue _sortedPlaylist = CreateQueueItemsFromArray(items); if (ShuffleMode.Equals(GroupShuffleMode.Shuffle)) { - _shuffledPlaylist = new List<QueueItem>(_sortedPlaylist); + _shuffledPlaylist = new List<SyncPlayQueueItem>(_sortedPlaylist); _shuffledPlaylist.Shuffle(); } @@ -125,14 +125,14 @@ namespace MediaBrowser.Controller.SyncPlay.Queue { if (PlayingItemIndex == NoPlayingItemIndex) { - _shuffledPlaylist = new List<QueueItem>(_sortedPlaylist); + _shuffledPlaylist = new List<SyncPlayQueueItem>(_sortedPlaylist); _shuffledPlaylist.Shuffle(); } else if (ShuffleMode.Equals(GroupShuffleMode.Sorted)) { // First time shuffle. var playingItem = _sortedPlaylist[PlayingItemIndex]; - _shuffledPlaylist = new List<QueueItem>(_sortedPlaylist); + _shuffledPlaylist = new List<SyncPlayQueueItem>(_sortedPlaylist); _shuffledPlaylist.RemoveAt(PlayingItemIndex); _shuffledPlaylist.Shuffle(); _shuffledPlaylist.Insert(0, playingItem); @@ -180,7 +180,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue _shuffledPlaylist.Clear(); LastChange = DateTime.UtcNow; - if (!clearPlayingItem && playingItem != null) + if (!clearPlayingItem && playingItem is not null) { _sortedPlaylist.Add(playingItem); if (ShuffleMode.Equals(GroupShuffleMode.Shuffle)) @@ -298,7 +298,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue LastChange = DateTime.UtcNow; - if (playingItem != null) + if (playingItem is not null) { if (playlistItemIds.Contains(playingItem.PlaylistItemId)) { @@ -313,17 +313,13 @@ namespace MediaBrowser.Controller.SyncPlay.Queue return true; } - else - { - // Restoring playing item. - SetPlayingItemByPlaylistId(playingItem.PlaylistItemId); - return false; - } - } - else - { + + // Restoring playing item. + SetPlayingItemByPlaylistId(playingItem.PlaylistItemId); return false; } + + return false; } /// <summary> @@ -411,7 +407,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue /// 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() + public SyncPlayQueueItem GetNextItemPlaylistId() { int newIndex; var playlist = GetPlaylistInternal(); @@ -506,12 +502,12 @@ namespace MediaBrowser.Controller.SyncPlay.Queue /// 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) + private List<SyncPlayQueueItem> CreateQueueItemsFromArray(IReadOnlyList<Guid> items) { - var list = new List<QueueItem>(); + var list = new List<SyncPlayQueueItem>(); foreach (var item in items) { - var queueItem = new QueueItem(item); + var queueItem = new SyncPlayQueueItem(item); list.Add(queueItem); } @@ -522,36 +518,33 @@ namespace MediaBrowser.Controller.SyncPlay.Queue /// Gets the current playlist considering the shuffle mode. /// </summary> /// <returns>The playlist.</returns> - private List<QueueItem> GetPlaylistInternal() + private List<SyncPlayQueueItem> GetPlaylistInternal() { if (ShuffleMode.Equals(GroupShuffleMode.Shuffle)) { return _shuffledPlaylist; } - else - { - return _sortedPlaylist; - } + + return _sortedPlaylist; } /// <summary> /// Gets the current playing item, depending on the shuffle mode. /// </summary> /// <returns>The playing item.</returns> - private QueueItem GetPlayingItem() + private SyncPlayQueueItem GetPlayingItem() { if (PlayingItemIndex == NoPlayingItemIndex) { return null; } - else if (ShuffleMode.Equals(GroupShuffleMode.Shuffle)) + + if (ShuffleMode.Equals(GroupShuffleMode.Shuffle)) { return _shuffledPlaylist[PlayingItemIndex]; } - else - { - return _sortedPlaylist[PlayingItemIndex]; - } + + return _sortedPlaylist[PlayingItemIndex]; } } } |
