diff options
Diffstat (limited to 'MediaBrowser.Controller')
89 files changed, 1795 insertions, 586 deletions
diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs index a56d3c822..81b532fda 100644 --- a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs +++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System.Threading.Tasks; @@ -23,7 +21,7 @@ namespace MediaBrowser.Controller.Authentication public interface IRequiresResolvedUser { - Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser); + Task<ProviderAuthenticationResult> Authenticate(string username, string password, User? resolvedUser); } public interface IHasNewUserPolicy @@ -33,8 +31,8 @@ namespace MediaBrowser.Controller.Authentication public class ProviderAuthenticationResult { - public string Username { get; set; } + public required string Username { get; set; } - public string DisplayName { get; set; } + public string? DisplayName { get; set; } } } diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs index ed7c2c2c1..b263c173e 100644 --- a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs +++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs @@ -1,11 +1,10 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Threading; using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; using MediaBrowser.Model.Configuration; namespace MediaBrowser.Controller.BaseItemManager @@ -15,8 +14,6 @@ namespace MediaBrowser.Controller.BaseItemManager { private readonly IServerConfigurationManager _serverConfigurationManager; - private int _metadataRefreshConcurrency; - /// <summary> /// Initializes a new instance of the <see cref="BaseItemManager"/> class. /// </summary> @@ -24,17 +21,9 @@ namespace MediaBrowser.Controller.BaseItemManager public BaseItemManager(IServerConfigurationManager serverConfigurationManager) { _serverConfigurationManager = serverConfigurationManager; - - _metadataRefreshConcurrency = GetMetadataRefreshConcurrency(); - SetupMetadataThrottler(); - - _serverConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated; } /// <inheritdoc /> - public SemaphoreSlim MetadataRefreshThrottler { get; private set; } - - /// <inheritdoc /> public bool IsMetadataFetcherEnabled(BaseItem baseItem, TypeOptions? libraryTypeOptions, string name) { if (baseItem is Channel) @@ -51,12 +40,11 @@ namespace MediaBrowser.Controller.BaseItemManager if (libraryTypeOptions is not null) { - return libraryTypeOptions.MetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); + return libraryTypeOptions.MetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase); } - var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, baseItem.GetType().Name, StringComparison.OrdinalIgnoreCase)); - - return itemConfig is null || !itemConfig.DisabledMetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); + var itemConfig = _serverConfigurationManager.GetMetadataOptionsForType(baseItem.GetType().Name); + return itemConfig is null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase); } /// <inheritdoc /> @@ -76,50 +64,11 @@ namespace MediaBrowser.Controller.BaseItemManager if (libraryTypeOptions is not null) { - 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 is null || !itemConfig.DisabledImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); - } - - /// <summary> - /// Called when the configuration is updated. - /// It will refresh the metadata throttler if the relevant config changed. - /// </summary> - private void OnConfigurationUpdated(object? sender, EventArgs e) - { - int newMetadataRefreshConcurrency = GetMetadataRefreshConcurrency(); - if (_metadataRefreshConcurrency != newMetadataRefreshConcurrency) - { - _metadataRefreshConcurrency = newMetadataRefreshConcurrency; - SetupMetadataThrottler(); - } - } - - /// <summary> - /// Creates the metadata refresh throttler. - /// </summary> - [MemberNotNull(nameof(MetadataRefreshThrottler))] - private void SetupMetadataThrottler() - { - MetadataRefreshThrottler = new SemaphoreSlim(_metadataRefreshConcurrency); - } - - /// <summary> - /// Returns the metadata refresh concurrency. - /// </summary> - private int GetMetadataRefreshConcurrency() - { - var concurrency = _serverConfigurationManager.Configuration.LibraryMetadataRefreshConcurrency; - - if (concurrency <= 0) - { - concurrency = Environment.ProcessorCount; + return libraryTypeOptions.ImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase); } - return concurrency; + var itemConfig = _serverConfigurationManager.GetMetadataOptionsForType(baseItem.GetType().Name); + return itemConfig is null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase); } } } diff --git a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs index b07c80879..ac20120d9 100644 --- a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs +++ b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs @@ -10,11 +10,6 @@ namespace MediaBrowser.Controller.BaseItemManager public interface IBaseItemManager { /// <summary> - /// Gets the semaphore used to limit the amount of concurrent metadata refreshes. - /// </summary> - SemaphoreSlim MetadataRefreshThrottler { get; } - - /// <summary> /// Is metadata fetcher enabled. /// </summary> /// <param name="baseItem">The base item.</param> diff --git a/MediaBrowser.Controller/Collections/ICollectionManager.cs b/MediaBrowser.Controller/Collections/ICollectionManager.cs index b8c33ee5a..38a78a67b 100644 --- a/MediaBrowser.Controller/Collections/ICollectionManager.cs +++ b/MediaBrowser.Controller/Collections/ICollectionManager.cs @@ -56,5 +56,12 @@ namespace MediaBrowser.Controller.Collections /// <param name="user">The user.</param> /// <returns>IEnumerable{BaseItem}.</returns> IEnumerable<BaseItem> CollapseItemsWithinBoxSets(IEnumerable<BaseItem> items, User user); + + /// <summary> + /// Gets the folder where collections are stored. + /// </summary> + /// <param name="createIfNeeded">Will create the collection folder on the storage if set to true.</param> + /// <returns>The folder instance referencing the collection storage.</returns> + Task<Folder?> GetCollectionsFolder(bool createIfNeeded); } } diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index e5ce0aa21..cdc3d52b9 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -66,7 +66,7 @@ namespace MediaBrowser.Controller.Drawing /// <returns>Guid.</returns> string GetImageCacheTag(BaseItem item, ItemImageInfo image); - string GetImageCacheTag(BaseItem item, ChapterInfo chapter); + string? GetImageCacheTag(BaseItem item, ChapterInfo chapter); string? GetImageCacheTag(User user); diff --git a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs index 62b70ce53..10326363a 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using MediaBrowser.Controller.Entities; @@ -9,12 +7,12 @@ namespace MediaBrowser.Controller.Drawing { public static class ImageProcessorExtensions { - public static string GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType) + public static string? GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType) { return processor.GetImageCacheTag(item, imageType, 0); } - public static string GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType, int imageIndex) + public static string? GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType, int imageIndex) { var imageInfo = item.GetImageInfo(imageType, imageIndex); diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index 2dbd513a1..237345206 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -183,6 +183,9 @@ namespace MediaBrowser.Controller.Entities.Audio progress.Report(percent * 95); } + // get album LUFS + LUFS = items.OfType<Audio>().Max(item => item.LUFS); + var parentRefreshOptions = refreshOptions; if (childUpdateType > ItemUpdateType.None) { diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 15a79fa1f..18d948a62 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -59,7 +59,7 @@ namespace MediaBrowser.Controller.Entities.Audio { if (IsAccessedByName) { - return new List<BaseItem>(); + return Enumerable.Empty<BaseItem>(); } return base.Children; diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index adc7b2f95..9f3e8eec9 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -129,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> @@ -1237,14 +1244,6 @@ namespace MediaBrowser.Controller.Entities return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken); } - protected virtual void TriggerOnRefreshStart() - { - } - - protected virtual void TriggerOnRefreshComplete() - { - } - /// <summary> /// Overrides the base implementation to refresh metadata for local trailers. /// </summary> @@ -1253,8 +1252,6 @@ namespace MediaBrowser.Controller.Entities /// <returns>true if a provider reports we changed.</returns> public async Task<ItemUpdateType> RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellationToken) { - TriggerOnRefreshStart(); - var requiresSave = false; if (SupportsOwnedItems) @@ -1274,21 +1271,14 @@ namespace MediaBrowser.Controller.Entities } } - try - { - var refreshOptions = requiresSave - ? new MetadataRefreshOptions(options) - { - ForceSave = true - } - : options; + var refreshOptions = requiresSave + ? new MetadataRefreshOptions(options) + { + ForceSave = true + } + : options; - return await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false); - } - finally - { - TriggerOnRefreshComplete(); - } + return await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false); } protected bool IsVisibleStandaloneInternal(User user, bool checkFolders) @@ -1360,7 +1350,7 @@ namespace MediaBrowser.Controller.Entities private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, IReadOnlyList<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) { var extras = LibraryManager.FindExtras(item, fileSystemChildren, options.DirectoryService).ToArray(); - var newExtraIds = extras.Select(i => i.Id).ToArray(); + var newExtraIds = Array.ConvertAll(extras, x => x.Id); var extrasChanged = !item.ExtraIds.SequenceEqual(newExtraIds); if (!extrasChanged && !options.ReplaceAllMetadata && options.MetadataRefreshMode != MetadataRefreshMode.FullRefresh) @@ -1874,7 +1864,7 @@ namespace MediaBrowser.Controller.Entities /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops.</exception> public bool HasImage(ImageType type, int imageIndex) { - return GetImageInfo(type, imageIndex) != null; + return GetImageInfo(type, imageIndex) is not null; } public void SetImage(ItemImageInfo image, int index) diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index 095b261c0..f51162f9d 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -3,6 +3,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -29,7 +30,7 @@ namespace MediaBrowser.Controller.Entities public class CollectionFolder : Folder, ICollectionFolder { private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - private static readonly Dictionary<string, LibraryOptions> _libraryOptions = new Dictionary<string, LibraryOptions>(); + private static readonly ConcurrentDictionary<string, LibraryOptions> _libraryOptions = new ConcurrentDictionary<string, LibraryOptions>(); private bool _requiresRefresh; /// <summary> @@ -139,45 +140,26 @@ namespace MediaBrowser.Controller.Entities } public static LibraryOptions GetLibraryOptions(string path) - { - lock (_libraryOptions) - { - if (!_libraryOptions.TryGetValue(path, out var options)) - { - options = LoadLibraryOptions(path); - _libraryOptions[path] = options; - } - - return options; - } - } + => _libraryOptions.GetOrAdd(path, LoadLibraryOptions); public static void SaveLibraryOptions(string path, LibraryOptions options) { - lock (_libraryOptions) - { - _libraryOptions[path] = options; + _libraryOptions[path] = options; - var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions); - foreach (var mediaPath in clone.PathInfos) + var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions); + foreach (var mediaPath in clone.PathInfos) + { + if (!string.IsNullOrEmpty(mediaPath.Path)) { - if (!string.IsNullOrEmpty(mediaPath.Path)) - { - mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path); - } + mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path); } - - XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path)); } + + XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path)); } public static void OnCollectionFolderChange() - { - lock (_libraryOptions) - { - _libraryOptions.Clear(); - } - } + => _libraryOptions.Clear(); public override bool IsSaveLocalMetadataEnabled() { diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index bccb4107f..44fe65103 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -301,14 +301,6 @@ namespace MediaBrowser.Controller.Entities return dictionary; } - protected override void TriggerOnRefreshStart() - { - } - - protected override void TriggerOnRefreshComplete() - { - } - /// <summary> /// Validates the children internal. /// </summary> @@ -510,26 +502,17 @@ namespace MediaBrowser.Controller.Entities private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken) { - // limit the amount of concurrent metadata refreshes - await ProviderManager.RunMetadataRefresh( - async () => - { - var series = container as Series; - if (series is not null) - { - await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); - } + if (container is Series series) + { + await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + } - await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false); - }, - cancellationToken).ConfigureAwait(false); + await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false); } private async Task RefreshChildMetadata(BaseItem child, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken) { - var container = child as IMetadataContainer; - - if (container is not null) + if (child is IMetadataContainer container) { await RefreshAllMetadataForContainer(container, refreshOptions, progress, cancellationToken).ConfigureAwait(false); } @@ -537,10 +520,7 @@ namespace MediaBrowser.Controller.Entities { if (refreshOptions.RefreshItem(child)) { - // limit the amount of concurrent metadata refreshes - await ProviderManager.RunMetadataRefresh( - async () => await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false), - cancellationToken).ConfigureAwait(false); + await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); } if (recursive && child is Folder folder) @@ -586,7 +566,7 @@ namespace MediaBrowser.Controller.Entities } var fanoutConcurrency = ConfigurationManager.Configuration.LibraryScanFanoutConcurrency; - var parallelism = fanoutConcurrency == 0 ? Environment.ProcessorCount : fanoutConcurrency; + var parallelism = fanoutConcurrency > 0 ? fanoutConcurrency : 2 * Environment.ProcessorCount; var actionBlock = new ActionBlock<int>( async i => @@ -618,7 +598,7 @@ namespace MediaBrowser.Controller.Entities for (var i = 0; i < childrenCount; i++) { - actionBlock.Post(i); + await actionBlock.SendAsync(i).ConfigureAwait(false); } actionBlock.Complete(); @@ -730,7 +710,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; @@ -1272,7 +1252,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) diff --git a/MediaBrowser.Controller/Entities/ItemImageInfo.cs b/MediaBrowser.Controller/Entities/ItemImageInfo.cs index 0171af27c..1d45d4da0 100644 --- a/MediaBrowser.Controller/Entities/ItemImageInfo.cs +++ b/MediaBrowser.Controller/Entities/ItemImageInfo.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -14,7 +12,7 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the path. /// </summary> /// <value>The path.</value> - public string Path { get; set; } + public required string Path { get; set; } /// <summary> /// Gets or sets the type. @@ -36,9 +34,9 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the blurhash. /// </summary> /// <value>The blurhash.</value> - public string BlurHash { get; set; } + public string? BlurHash { get; set; } [JsonIgnore] - public bool IsLocalFile => Path is null || !Path.StartsWith("http", StringComparison.OrdinalIgnoreCase); + public bool IsLocalFile => !Path.StartsWith("http", StringComparison.OrdinalIgnoreCase); } } diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 597b4cecb..bf31508c1 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -99,7 +99,7 @@ namespace MediaBrowser.Controller.Entities.TV } [JsonIgnore] - public bool IsInSeasonFolder => FindParent<Season>() != null; + public bool IsInSeasonFolder => FindParent<Season>() is not null; [JsonIgnore] public string SeriesPresentationUniqueKey { get; set; } diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index e7a8a773e..a49c1609d 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] diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 5b7abea10..9f685b7e2 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -333,7 +333,7 @@ namespace MediaBrowser.Controller.Entities protected override bool IsActiveRecording() { - return LiveTvManager.GetActiveRecordingInfo(Path) != null; + return LiveTvManager.GetActiveRecordingInfo(Path) is not null; } public override bool CanDelete() diff --git a/MediaBrowser.Controller/Events/Authentication/AuthenticationRequestEventArgs.cs b/MediaBrowser.Controller/Events/Authentication/AuthenticationRequestEventArgs.cs new file mode 100644 index 000000000..2143c6998 --- /dev/null +++ b/MediaBrowser.Controller/Events/Authentication/AuthenticationRequestEventArgs.cs @@ -0,0 +1,60 @@ +using System; +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.Events.Authentication; + +/// <summary> +/// A class representing an authentication result event. +/// </summary> +public class AuthenticationRequestEventArgs : EventArgs +{ + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationRequestEventArgs"/> class. + /// </summary> + /// <param name="request">The <see cref="AuthenticationRequest"/>.</param> + public AuthenticationRequestEventArgs(AuthenticationRequest request) + { + Username = request.Username; + UserId = request.UserId; + App = request.App; + AppVersion = request.AppVersion; + DeviceId = request.DeviceId; + DeviceName = request.DeviceName; + RemoteEndPoint = request.RemoteEndPoint; + } + + /// <summary> + /// Gets or sets the user name. + /// </summary> + public string? Username { get; set; } + + /// <summary> + /// Gets or sets the user id. + /// </summary> + public Guid? UserId { get; set; } + + /// <summary> + /// Gets or sets the app. + /// </summary> + public string? App { get; set; } + + /// <summary> + /// Gets or sets the app version. + /// </summary> + public string? AppVersion { get; set; } + + /// <summary> + /// Gets or sets the device id. + /// </summary> + public string? DeviceId { get; set; } + + /// <summary> + /// Gets or sets the device name. + /// </summary> + public string? DeviceName { get; set; } + + /// <summary> + /// Gets or sets the remote endpoint. + /// </summary> + public string? RemoteEndPoint { get; set; } +} diff --git a/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs b/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs new file mode 100644 index 000000000..357ef9406 --- /dev/null +++ b/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs @@ -0,0 +1,38 @@ +using System; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Dto; + +namespace MediaBrowser.Controller.Events.Authentication; + +/// <summary> +/// A class representing an authentication result event. +/// </summary> +public class AuthenticationResultEventArgs : EventArgs +{ + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationResultEventArgs"/> class. + /// </summary> + /// <param name="result">The <see cref="AuthenticationResult"/>.</param> + public AuthenticationResultEventArgs(AuthenticationResult result) + { + User = result.User; + SessionInfo = result.SessionInfo; + ServerId = result.ServerId; + } + + /// <summary> + /// Gets or sets the user. + /// </summary> + public UserDto User { get; set; } + + /// <summary> + /// Gets or sets the session information. + /// </summary> + public SessionInfo? SessionInfo { get; set; } + + /// <summary> + /// Gets or sets the server id. + /// </summary> + public string? ServerId { get; set; } +} diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs index 3b5e8ece7..6c58064ce 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> @@ -115,5 +120,13 @@ namespace MediaBrowser.Controller.Extensions /// <returns>The unix socket permissions.</returns> 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/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 11afdc4ae..45ac5c3a8 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -4,7 +4,6 @@ using System.Net; using MediaBrowser.Common; -using MediaBrowser.Common.Net; using MediaBrowser.Model.System; using Microsoft.AspNetCore.Http; @@ -75,10 +74,10 @@ namespace MediaBrowser.Controller /// <summary> /// Gets an URL that can be used to access the API over LAN. /// </summary> - /// <param name="hostname">An optional hostname to use.</param> + /// <param name="ipAddress">An optional IP address to use.</param> /// <param name="allowHttps">A value indicating whether to allow HTTPS.</param> /// <returns>The API URL.</returns> - string GetApiUrlForLocalAccess(IPObject hostname = null, bool allowHttps = true); + string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true); /// <summary> /// Gets a local (LAN) URL that can be used to access the API. diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index 37b4afcf3..6d6a532db 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -97,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> @@ -112,15 +105,6 @@ 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> diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs index c70102167..dcd0110fb 100644 --- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs +++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs @@ -217,7 +217,7 @@ namespace MediaBrowser.Controller.Library /// <returns><c>true</c> if [contains file system entry by name] [the specified name]; otherwise, <c>false</c>.</returns> public bool ContainsFileSystemEntryByName(string name) { - return GetFileSystemEntryByName(name) != null; + return GetFileSystemEntryByName(name) is not null; } public string GetCollectionType() diff --git a/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs b/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs index 41cfcae16..ee9420cb4 100644 --- a/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs +++ b/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs @@ -1,8 +1,8 @@ -#nullable disable - #pragma warning disable CS1591 +using System; using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Configuration; namespace MediaBrowser.Controller.Library @@ -10,8 +10,15 @@ namespace MediaBrowser.Controller.Library public static class MetadataConfigurationExtensions { public static MetadataConfiguration GetMetadataConfiguration(this IConfigurationManager config) - { - return config.GetConfiguration<MetadataConfiguration>("metadata"); - } + => config.GetConfiguration<MetadataConfiguration>("metadata"); + + /// <summary> + /// Gets the <see cref="MetadataOptions" /> for the specified type. + /// </summary> + /// <param name="config">The <see cref="IServerConfigurationManager"/>.</param> + /// <param name="type">The type to get the <see cref="MetadataOptions" /> for.</param> + /// <returns>The <see cref="MetadataOptions" /> for the specified type or <c>null</c>.</returns> + public static MetadataOptions? GetMetadataOptionsForType(this IServerConfigurationManager config, string type) + => Array.Find(config.Configuration.MetadataOptions, i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)); } } diff --git a/MediaBrowser.Controller/Lyrics/ILyricParser.cs b/MediaBrowser.Controller/Lyrics/ILyricParser.cs new file mode 100644 index 000000000..65a9471a3 --- /dev/null +++ b/MediaBrowser.Controller/Lyrics/ILyricParser.cs @@ -0,0 +1,28 @@ +using MediaBrowser.Controller.Resolvers; +using MediaBrowser.Providers.Lyric; + +namespace MediaBrowser.Controller.Lyrics; + +/// <summary> +/// Interface ILyricParser. +/// </summary> +public interface ILyricParser +{ + /// <summary> + /// Gets a value indicating the provider name. + /// </summary> + string Name { get; } + + /// <summary> + /// Gets the priority. + /// </summary> + /// <value>The priority.</value> + ResolverPriority Priority { get; } + + /// <summary> + /// Parses the raw lyrics into a response. + /// </summary> + /// <param name="lyrics">The raw lyrics content.</param> + /// <returns>The parsed lyrics or null if invalid.</returns> + LyricResponse? ParseLyrics(LyricFile lyrics); +} diff --git a/MediaBrowser.Controller/Lyrics/ILyricProvider.cs b/MediaBrowser.Controller/Lyrics/ILyricProvider.cs deleted file mode 100644 index 2a04c6152..000000000 --- a/MediaBrowser.Controller/Lyrics/ILyricProvider.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Resolvers; - -namespace MediaBrowser.Controller.Lyrics; - -/// <summary> -/// Interface ILyricsProvider. -/// </summary> -public interface ILyricProvider -{ - /// <summary> - /// Gets a value indicating the provider name. - /// </summary> - string Name { get; } - - /// <summary> - /// Gets the priority. - /// </summary> - /// <value>The priority.</value> - ResolverPriority Priority { get; } - - /// <summary> - /// Gets the supported media types for this provider. - /// </summary> - /// <value>The supported media types.</value> - IReadOnlyCollection<string> SupportedMediaTypes { get; } - - /// <summary> - /// Gets the lyrics. - /// </summary> - /// <param name="item">The media item.</param> - /// <returns>A task representing found lyrics.</returns> - Task<LyricResponse?> GetLyrics(BaseItem item); -} diff --git a/MediaBrowser.Controller/Lyrics/LyricFile.cs b/MediaBrowser.Controller/Lyrics/LyricFile.cs new file mode 100644 index 000000000..ede89403c --- /dev/null +++ b/MediaBrowser.Controller/Lyrics/LyricFile.cs @@ -0,0 +1,28 @@ +namespace MediaBrowser.Providers.Lyric; + +/// <summary> +/// The information for a raw lyrics file before parsing. +/// </summary> +public class LyricFile +{ + /// <summary> + /// Initializes a new instance of the <see cref="LyricFile"/> class. + /// </summary> + /// <param name="name">The name.</param> + /// <param name="content">The content, must not be empty.</param> + public LyricFile(string name, string content) + { + Name = name; + Content = content; + } + + /// <summary> + /// Gets or sets the name of the lyrics file. This must include the file extension. + /// </summary> + public string Name { get; set; } + + /// <summary> + /// Gets or sets the contents of the file. + /// </summary> + public string Content { get; set; } +} diff --git a/MediaBrowser.Controller/Lyrics/LyricInfo.cs b/MediaBrowser.Controller/Lyrics/LyricInfo.cs deleted file mode 100644 index 6ec6df582..000000000 --- a/MediaBrowser.Controller/Lyrics/LyricInfo.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.IO; -using Jellyfin.Extensions; - -namespace MediaBrowser.Controller.Lyrics; - -/// <summary> -/// Lyric helper methods. -/// </summary> -public static class LyricInfo -{ - /// <summary> - /// Gets matching lyric file for a requested item. - /// </summary> - /// <param name="lyricProvider">The lyricProvider interface to use.</param> - /// <param name="itemPath">Path of requested item.</param> - /// <returns>Lyric file path if passed lyric provider's supported media type is found; otherwise, null.</returns> - public static string? GetLyricFilePath(this ILyricProvider lyricProvider, string itemPath) - { - // Ensure we have a provider - if (lyricProvider is null) - { - return null; - } - - // Ensure the path to the item is not null - string? itemDirectoryPath = Path.GetDirectoryName(itemPath); - if (itemDirectoryPath is null) - { - return null; - } - - // Ensure the directory path exists - if (!Directory.Exists(itemDirectoryPath)) - { - return null; - } - - foreach (var lyricFilePath in Directory.GetFiles(itemDirectoryPath, $"{Path.GetFileNameWithoutExtension(itemPath)}.*")) - { - if (lyricProvider.SupportedMediaTypes.Contains(Path.GetExtension(lyricFilePath.AsSpan())[1..], StringComparison.OrdinalIgnoreCase)) - { - return lyricFilePath; - } - } - - return null; - } -} diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 70018bb36..14417a5e4 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -23,7 +23,7 @@ using Microsoft.Extensions.Configuration; namespace MediaBrowser.Controller.MediaEncoding { - public class EncodingHelper + public partial class EncodingHelper { private const string QsvAlias = "qs"; private const string VaapiAlias = "va"; @@ -37,7 +37,8 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly IMediaEncoder _mediaEncoder; private readonly ISubtitleEncoder _subtitleEncoder; private readonly IConfiguration _config; - private readonly Version _minKernelVersionAmdVkFmtModifier = new Version(5, 15); + private readonly IConfigurationManager _configurationManager; + // 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); @@ -45,6 +46,9 @@ namespace MediaBrowser.Controller.MediaEncoding 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 readonly Version _minFFmpegSvtAv1Params = new Version(5, 1); + private readonly Version _minFFmpegVaapiH26xEncA53CcSei = new Version(6, 0); private static readonly string[] _videoProfilesH264 = new[] { @@ -64,6 +68,13 @@ namespace MediaBrowser.Controller.MediaEncoding "Main10" }; + private static readonly string[] _videoProfilesAv1 = new[] + { + "Main", + "High", + "Professional", + }; + private static readonly HashSet<string> _mp4ContainerNames = new(StringComparer.OrdinalIgnoreCase) { "mp4", @@ -103,21 +114,29 @@ namespace MediaBrowser.Controller.MediaEncoding IApplicationPaths appPaths, IMediaEncoder mediaEncoder, ISubtitleEncoder subtitleEncoder, - IConfiguration config) + IConfiguration config, + IConfigurationManager configurationManager) { _appPaths = appPaths; _mediaEncoder = mediaEncoder; _subtitleEncoder = subtitleEncoder; _config = config; + _configurationManager = configurationManager; } + [GeneratedRegex(@"\s+")] + private static partial Regex WhiteSpaceRegex(); + public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions) - => GetH264OrH265Encoder("libx264", "h264", state, encodingOptions); + => GetH26xOrAv1Encoder("libx264", "h264", state, encodingOptions); public string GetH265Encoder(EncodingJobInfo state, EncodingOptions encodingOptions) - => GetH264OrH265Encoder("libx265", "hevc", state, encodingOptions); + => GetH26xOrAv1Encoder("libx265", "hevc", state, encodingOptions); - private string GetH264OrH265Encoder(string defaultEncoder, string hwEncoder, EncodingJobInfo state, EncodingOptions encodingOptions) + public string GetAv1Encoder(EncodingJobInfo state, EncodingOptions encodingOptions) + => GetH26xOrAv1Encoder("libsvtav1", "av1", state, encodingOptions); + + private string GetH26xOrAv1Encoder(string defaultEncoder, string hwEncoder, EncodingJobInfo state, EncodingOptions encodingOptions) { // Only use alternative encoders for video files. // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully @@ -162,7 +181,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") @@ -207,8 +227,8 @@ namespace MediaBrowser.Controller.MediaEncoding } if (string.Equals(state.VideoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase) - && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase) - && string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase)) + && state.VideoStream.VideoRange == VideoRange.HDR + && state.VideoStream.VideoRangeType == VideoRangeType.DOVI) { // Only native SW decoder and HW accelerator can parse dovi rpu. var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty; @@ -219,9 +239,9 @@ namespace MediaBrowser.Controller.MediaEncoding return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder; } - return string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase) - && (string.Equals(state.VideoStream.VideoRangeType, "HDR10", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.VideoStream.VideoRangeType, "HLG", StringComparison.OrdinalIgnoreCase)); + return state.VideoStream.VideoRange == VideoRange.HDR + && (state.VideoStream.VideoRangeType == VideoRangeType.HDR10 + || state.VideoStream.VideoRangeType == VideoRangeType.HLG); } private bool IsVulkanHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options) @@ -233,7 +253,7 @@ namespace MediaBrowser.Controller.MediaEncoding // libplacebo has partial Dolby Vision to SDR tonemapping support. return options.EnableTonemapping - && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase) + && state.VideoStream.VideoRange == VideoRange.HDR && GetVideoColorBitDepth(state) == 10; } @@ -248,8 +268,8 @@ namespace MediaBrowser.Controller.MediaEncoding // Native VPP tonemapping may come to QSV in the future. - return string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase) - && string.Equals(state.VideoStream.VideoRangeType, "HDR10", StringComparison.OrdinalIgnoreCase); + return state.VideoStream.VideoRange == VideoRange.HDR + && state.VideoStream.VideoRangeType == VideoRangeType.HDR10; } /// <summary> @@ -264,6 +284,11 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(codec)) { + if (string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase)) + { + return GetAv1Encoder(state, encodingOptions); + } + if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) { @@ -563,6 +588,11 @@ namespace MediaBrowser.Controller.MediaEncoding return Array.FindIndex(_videoProfilesH265, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase)); } + if (string.Equals("av1", videoCodec, StringComparison.OrdinalIgnoreCase)) + { + return Array.FindIndex(_videoProfilesAv1, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase)); + } + return -1; } @@ -712,28 +742,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()) @@ -754,9 +799,12 @@ namespace MediaBrowser.Controller.MediaEncoding public string GetGraphicalSubCanvasSize(EncodingJobInfo state) { + // 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; @@ -824,21 +872,17 @@ 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 (_mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965) @@ -851,19 +895,30 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (_mediaEncoder.IsVaapiDeviceAmd) { + // Disable AMD EFC feature since it's still unstable in upstream Mesa. + Environment.SetEnvironmentVariable("AMD_DEBUG", "noefc"); + if (IsVulkanFullSupported() - && _mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier - && Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier) + && _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop) { + 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. - args.Append(GetVulkanDeviceArgs(0, null, VaapiAlias, VulkanAlias)); filterDevArgs = GetFilterHwDeviceArgs(VulkanAlias); } - else if (doOclTonemap) + else { - // ROCm/ROCr OpenCL runtime - args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias)); - filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias); + 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 if (doOclTonemap) @@ -1007,7 +1062,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay) { - var tmpConcatPath = Path.Join(options.TranscodingTempPath, state.MediaSource.Id + ".concat"); + var tmpConcatPath = Path.Join(_configurationManager.GetTranscodePath(), state.MediaSource.Id + ".concat"); _mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath); arg.Append(" -f concat -safe 0 -i ") .Append(tmpConcatPath); @@ -1162,6 +1217,12 @@ namespace MediaBrowser.Controller.MediaEncoding int bitrate = state.OutputVideoBitrate.Value; + // Bit rate under 1000k is not allowed in h264_qsv + if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + { + bitrate = Math.Max(bitrate, 1000); + } + // Currently use the same buffer size for all encoders int bufsize = bitrate * 2; @@ -1179,6 +1240,11 @@ namespace MediaBrowser.Controller.MediaEncoding return FormattableString.Invariant($" -b:v {bitrate}"); } + if (string.Equals(videoCodec, "libsvtav1", StringComparison.OrdinalIgnoreCase)) + { + return FormattableString.Invariant($" -b:v {bitrate} -bufsize {bufsize}"); + } + if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "libx265", StringComparison.OrdinalIgnoreCase)) { @@ -1186,14 +1252,16 @@ namespace MediaBrowser.Controller.MediaEncoding } if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase)) + || string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoCodec, "av1_amf", StringComparison.OrdinalIgnoreCase)) { // Override the too high default qmin 18 in transcoding preset return FormattableString.Invariant($" -rc cbr -qmin 0 -qmax 32 -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}"); } if (string.Equals(videoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoCodec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)) + || string.Equals(videoCodec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoCodec, "av1_vaapi", StringComparison.OrdinalIgnoreCase)) { // VBR in i965 driver may result in pixelated output. if (_mediaEncoder.IsVaapiDeviceInteli965) @@ -1211,14 +1279,23 @@ namespace MediaBrowser.Controller.MediaEncoding { if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel)) { - if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)) + { + // Transcode to level 5.3 (15) and lower for maximum compatibility. + // https://en.wikipedia.org/wiki/AV1#Levels + if (requestLevel < 0 || requestLevel >= 15) + { + return "15"; + } + } + else if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)) { // Transcode to level 5.0 and lower for maximum compatibility. // Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it. // https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels // MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880. - if (requestLevel >= 150) + if (requestLevel < 0 || requestLevel >= 150) { return "150"; } @@ -1228,7 +1305,7 @@ namespace MediaBrowser.Controller.MediaEncoding // Transcode to level 5.1 and lower for maximum compatibility. // h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4. // https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels - if (requestLevel >= 51) + if (requestLevel < 0 || requestLevel >= 51) { return "51"; } @@ -1340,22 +1417,11 @@ namespace MediaBrowser.Controller.MediaEncoding { var args = string.Empty; var gopArg = string.Empty; - var keyFrameArg = string.Empty; - if (isEventPlaylist) - { - keyFrameArg = string.Format( - CultureInfo.InvariantCulture, - " -force_key_frames:0 \"expr:gte(t,n_forced*{0})\"", - segmentLength); - } - else if (startNumber.HasValue) - { - keyFrameArg = string.Format( - CultureInfo.InvariantCulture, - " -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"", - startNumber.Value * segmentLength, - segmentLength); - } + + var keyFrameArg = string.Format( + CultureInfo.InvariantCulture, + " -force_key_frames:0 \"expr:gte(t,n_forced*{0})\"", + segmentLength); var framerate = state.VideoStream?.RealFrameRate; if (framerate.HasValue) @@ -1377,14 +1443,18 @@ namespace MediaBrowser.Controller.MediaEncoding || string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "hevc_qsv", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "hevc_amf", StringComparison.OrdinalIgnoreCase)) + || string.Equals(codec, "av1_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "av1_nvenc", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "av1_amf", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "libsvtav1", StringComparison.OrdinalIgnoreCase)) { args += gopArg; } else if (string.Equals(codec, "libx264", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)) + || string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "av1_vaapi", StringComparison.OrdinalIgnoreCase)) { args += keyFrameArg; @@ -1399,6 +1469,13 @@ namespace MediaBrowser.Controller.MediaEncoding args += keyFrameArg + gopArg; } + // global_header produced by AMD HEVC VA-API encoder causes non-playable fMP4 on iOS + if (string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase) + && _mediaEncoder.IsVaapiDeviceAmd) + { + args += " -flags:v -global_header"; + } + return args; } @@ -1520,18 +1597,60 @@ namespace MediaBrowser.Controller.MediaEncoding param += " -crf " + defaultCrf; } } + else if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase)) + { + // Default to use the recommended preset 10. + // Omit presets < 5, which are too slow for on the fly encoding. + // https://gitlab.com/AOMediaCodec/SVT-AV1/-/blob/master/Docs/Ffmpeg.md + param += encodingOptions.EncoderPreset switch + { + "veryslow" => " -preset 5", + "slower" => " -preset 6", + "slow" => " -preset 7", + "medium" => " -preset 8", + "fast" => " -preset 9", + "faster" => " -preset 10", + "veryfast" => " -preset 11", + "superfast" => " -preset 12", + "ultrafast" => " -preset 13", + _ => " -preset 10" + }; + } + else if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "av1_vaapi", StringComparison.OrdinalIgnoreCase)) + { + // -compression_level is not reliable on AMD. + if (_mediaEncoder.IsVaapiDeviceInteliHD) + { + param += encodingOptions.EncoderPreset switch + { + "veryslow" => " -compression_level 1", + "slower" => " -compression_level 2", + "slow" => " -compression_level 3", + "medium" => " -compression_level 4", + "fast" => " -compression_level 5", + "faster" => " -compression_level 6", + "veryfast" => " -compression_level 7", + "superfast" => " -compression_level 7", + "ultrafast" => " -compression_level 7", + _ => string.Empty + }; + } + } else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) // h264 (h264_qsv) - || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_qsv) + || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase) // hevc (hevc_qsv) + || string.Equals(videoEncoder, "av1_qsv", StringComparison.OrdinalIgnoreCase)) // av1 (av1_qsv) { - string[] valid_h264_qsv = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" }; + string[] valid_presets = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" }; - if (valid_h264_qsv.Contains(encodingOptions.EncoderPreset, StringComparison.OrdinalIgnoreCase)) + if (valid_presets.Contains(encodingOptions.EncoderPreset, StringComparison.OrdinalIgnoreCase)) { param += " -preset " + encodingOptions.EncoderPreset; } else { - param += " -preset 7"; + param += " -preset veryfast"; } // Only h264_qsv has look_ahead option @@ -1541,7 +1660,8 @@ namespace MediaBrowser.Controller.MediaEncoding } } else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc) - || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_nvenc) + || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) // hevc (hevc_nvenc) + || string.Equals(videoEncoder, "av1_nvenc", StringComparison.OrdinalIgnoreCase)) // av1 (av1_nvenc) { switch (encodingOptions.EncoderPreset) { @@ -1549,11 +1669,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; @@ -1581,13 +1701,14 @@ namespace MediaBrowser.Controller.MediaEncoding } } else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf) - || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_amf) + || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) // hevc (hevc_amf) + || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase)) // av1 (av1_amf) { switch (encodingOptions.EncoderPreset) { case "veryslow": - case "slow": case "slower": + case "slow": param += " -quality quality"; break; @@ -1608,9 +1729,15 @@ namespace MediaBrowser.Controller.MediaEncoding break; } + if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase)) + { + param += " -header_insertion_mode gop"; + } + if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) { - param += " -header_insertion_mode gop -gops_per_idr 1"; + param += " -gops_per_idr 1"; } } else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // vp8 @@ -1719,7 +1846,7 @@ namespace MediaBrowser.Controller.MediaEncoding } var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault() ?? string.Empty; - profile = Regex.Replace(profile, @"\s+", string.Empty); + profile = WhiteSpaceRegex().Replace(profile, string.Empty); // We only transcode to HEVC 8-bit for now, force Main Profile. if (profile.Contains("main10", StringComparison.OrdinalIgnoreCase) @@ -1741,6 +1868,14 @@ namespace MediaBrowser.Controller.MediaEncoding profile = "high"; } + // We only need Main profile of AV1 encoders. + if (videoEncoder.Contains("av1", StringComparison.OrdinalIgnoreCase) + && (profile.Contains("high", StringComparison.OrdinalIgnoreCase) + || profile.Contains("professional", StringComparison.OrdinalIgnoreCase))) + { + profile = "main"; + } + // h264_vaapi does not support Baseline profile, force Constrained Baseline in this case, // which is compatible (and ugly). if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) @@ -1782,7 +1917,9 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(profile)) { - if (!string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) + // Currently there's no profile option in av1_nvenc encoder + if (!(string.Equals(videoEncoder, "av1_nvenc", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))) { param += " -profile:v:0 " + profile; } @@ -1808,19 +1945,41 @@ namespace MediaBrowser.Controller.MediaEncoding param += " -level " + (hevcLevel / 3); } } + else if (string.Equals(videoEncoder, "av1_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase)) + { + // libsvtav1 and av1_qsv use -level 60 instead of -level 16 + // https://aomedia.org/av1/specification/annex-a/ + if (int.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out int av1Level)) + { + var x = 2 + (av1Level >> 2); + var y = av1Level & 3; + var res = (x * 10) + y; + param += " -level " + res; + } + } else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) + || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase)) { param += " -level " + level; } else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)) + || string.Equals(videoEncoder, "av1_nvenc", StringComparison.OrdinalIgnoreCase)) { // level option may cause NVENC to fail. // NVENC cannot adjust the given level, just throw an error. + } + else if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "av1_vaapi", StringComparison.OrdinalIgnoreCase)) + { // level option may cause corrupted frames on AMD VAAPI. + if (_mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965) + { + param += " -level " + level; + } } else if (!string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase)) { @@ -1842,6 +2001,20 @@ namespace MediaBrowser.Controller.MediaEncoding param += " -x265-params:0 no-info=1"; } + if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase) + && _mediaEncoder.EncoderVersion >= _minFFmpegSvtAv1Params) + { + param += " -svtav1-params:0 rc=1:tune=0:film-grain=0:enable-overlays=1:enable-tf=0"; + } + + /* Access unit too large: 8192 < 20880 error */ + if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) || + string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)) && + _mediaEncoder.EncoderVersion >= _minFFmpegVaapiH26xEncA53CcSei) + { + param += " -sei -a53_cc"; + } + return param; } @@ -1920,12 +2093,12 @@ namespace MediaBrowser.Controller.MediaEncoding var requestedRangeTypes = state.GetRequestedRangeTypes(videoStream.Codec); if (requestedRangeTypes.Length > 0) { - if (string.IsNullOrEmpty(videoStream.VideoRangeType)) + if (videoStream.VideoRangeType == VideoRangeType.Unknown) { return false; } - if (!requestedRangeTypes.Contains(videoStream.VideoRangeType, StringComparison.OrdinalIgnoreCase)) + if (!requestedRangeTypes.Contains(videoStream.VideoRangeType.ToString(), StringComparison.OrdinalIgnoreCase)) { return false; } @@ -2148,7 +2321,7 @@ namespace MediaBrowser.Controller.MediaEncoding var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec); // Don't scale the real bitrate lower than the requested bitrate - var scaleFactor = Math.Min(outputScaleFactor / inputScaleFactor, 1); + var scaleFactor = Math.Max(outputScaleFactor / inputScaleFactor, 1); if (bitrate <= 500000) { @@ -2539,7 +2712,7 @@ namespace MediaBrowser.Controller.MediaEncoding string args = string.Empty; // http://ffmpeg.org/ffmpeg-all.html#toc-Complex-filtergraphs-1 - if (state.VideoStream != null && videoProcessFilters.Contains("-filter_complex", StringComparison.Ordinal)) + if (state.VideoStream is not null && videoProcessFilters.Contains("-filter_complex", StringComparison.Ordinal)) { int videoStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.VideoStream); @@ -2929,7 +3102,7 @@ namespace MediaBrowser.Controller.MediaEncoding 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)) { @@ -2941,7 +3114,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={1}:c={2},tonemap_vaapi=format={0}:p=bt709:t=bt709:m=bt709:extra_hw_frames=32"; + return string.Format( CultureInfo.InvariantCulture, args, @@ -2949,37 +3123,28 @@ namespace MediaBrowser.Controller.MediaEncoding options.VppTonemappingBrightness, options.VppTonemappingContrast); } - - if (string.Equals(hwTonemapSuffix, "vulkan", StringComparison.OrdinalIgnoreCase)) + else { - args = "libplacebo=format={1}:tonemapping={2}:color_primaries=bt709:color_trc=bt709:colorspace=bt709:peak_detect=0:upscaler=none:downscaler=none"; - - if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase)) - { - args += ":range={6}"; - } + args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709:tonemap={2}:peak={3}:desat={4}"; - if (string.Equals(options.TonemappingAlgorithm, "bt2390", StringComparison.OrdinalIgnoreCase)) - { - algorithm = "bt.2390"; - } - else if (string.Equals(options.TonemappingAlgorithm, "none", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(options.TonemappingMode, "max", StringComparison.OrdinalIgnoreCase) + || string.Equals(options.TonemappingMode, "rgb", StringComparison.OrdinalIgnoreCase)) { - algorithm = "clip"; + if (_mediaEncoder.EncoderVersion >= _minFFmpegOclCuTonemapMode) + { + args += ":tonemap_mode={5}"; + } } - } - else - { - args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709:tonemap={2}:peak={3}:desat={4}"; 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}"; } } @@ -2991,10 +3156,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> @@ -3569,7 +3804,7 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add(swDeintFilter); } - var outFormat = doOclTonemap ? "yuv420p10le" : "yuv420p"; + var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12"); var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); // sw scale mainFilters.Add(swScaleFilter); @@ -3596,12 +3831,6 @@ namespace MediaBrowser.Controller.MediaEncoding // map from d3d11va to qsv. mainFilters.Add("hwmap=derive_device=qsv"); } - else - { - // Insert a qsv scaler to sync the decoder surface, - // msdk will passthrough this internally. - mainFilters.Add("hwmap=derive_device=qsv,scale_qsv"); - } } // hw deint @@ -3770,7 +3999,7 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add(swDeintFilter); } - var outFormat = doOclTonemap ? "yuv420p10le" : "yuv420p"; + var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12"); var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); // sw scale mainFilters.Add(swScaleFilter); @@ -3992,14 +4221,13 @@ namespace MediaBrowser.Controller.MediaEncoding // prefered vaapi + vulkan filters pipeline if (_mediaEncoder.IsVaapiDeviceAmd && isVaapiVkSupported - && _mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier - && Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier) + && _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop) { - // AMD radeonsi path(Vega/gfx9+, kernel>=5.15), with extra vulkan tonemap and overlay support. + // AMD radeonsi path(targeting Polaris/gfx8+), with extra vulkan tonemap and overlay support. return GetAmdVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder); } - // Intel i965 and Amd radeonsi/r600 path(Polaris/gfx8-), only featuring scale and deinterlace support. + // Intel i965 and Amd legacy driver path, only featuring scale and deinterlace support. return GetVaapiLimitedVidFiltersPrefered(state, options, vidDecoder, vidEncoder); } @@ -4224,7 +4452,6 @@ 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); @@ -4253,99 +4480,79 @@ 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) { - mainFilters.Add("hwupload=derive_device=vaapi"); - mainFilters.Add("format=vaapi"); - mainFilters.Add("hwmap=derive_device=vulkan"); + // sw => hw + mainFilters.Add("hwupload=derive_device=vulkan"); mainFilters.Add("format=vulkan"); } + else + { + // 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 (Polaris/gfx8+). + 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"); - mainFilters.Add("format=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 (Polaris/gfx8+). + mainFilters.Add("hwmap=derive_device=vaapi"); mainFilters.Add("format=vaapi"); - } - var memoryOutput = false; - var isUploadForVkTonemap = isSwDecoder && doVkTonemap; - if ((isVaapiDecoder && isSwEncoder) || isUploadForVkTonemap) - { - memoryOutput = true; + // clear the surf->meta_offset and output nv12 + mainFilters.Add("scale_vaapi=format=nv12"); - // OUTPUT nv12 surface(memory) - mainFilters.Add("hwdownload"); - mainFilters.Add("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"); } @@ -4354,55 +4561,51 @@ 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); - } - - // prefer vaapi hwupload to vulkan hwupload, - // Mesa RADV does not support a dedicated transfer queue. - subFilters.Add("hwupload=derive_device=vaapi"); - subFilters.Add("format=vaapi"); - subFilters.Add("hwmap=derive_device=vulkan"); - subFilters.Add("format=vulkan"); + // 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"); - // TODO: figure out why libplacebo can sync without vaSyncSurface VPP support in radeonsi. - overlayFilters.Add("libplacebo=format=nv12:apply_filmgrain=0:apply_dolbyvision=0:upscaler=none:downscaler=none:dithering=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 = 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 (Polaris/gfx8+). + 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); } } } @@ -5052,10 +5255,8 @@ namespace MediaBrowser.Controller.MediaEncoding 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) - + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + " -threads 3" + (isAv1 ? " -c:v av1" : string.Empty); + + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + " -threads 2" + (isAv1 ? " -c:v av1" : string.Empty); } } else @@ -5489,7 +5690,6 @@ namespace MediaBrowser.Controller.MediaEncoding // Apply -analyzeduration as per the environment variable, // otherwise ffmpeg will break on certain files due to default value is 0. - // The default value of -probesize is more than enough, so leave it as is. var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty; if (state.MediaSource.AnalyzeDurationMs > 0) @@ -5508,6 +5708,14 @@ namespace MediaBrowser.Controller.MediaEncoding inputModifier = inputModifier.Trim(); + // Apply -probesize if configured + var ffmpegProbeSize = _config.GetFFmpegProbeSize(); + + if (!string.IsNullOrEmpty(ffmpegProbeSize)) + { + inputModifier += $" -probesize {ffmpegProbeSize}"; + } + var userAgentParam = GetUserAgentParam(state); if (!string.IsNullOrEmpty(userAgentParam)) @@ -5755,19 +5963,25 @@ namespace MediaBrowser.Controller.MediaEncoding private void ShiftVideoCodecsIfNeeded(List<string> videoCodecs, EncodingOptions encodingOptions) { - // Shift hevc/h265 to the end of list if hevc encoding is not allowed. - if (encodingOptions.AllowHevcEncoding) + // No need to shift if there is only one supported video codec. + if (videoCodecs.Count < 2) { return; } - // No need to shift if there is only one supported video codec. - if (videoCodecs.Count < 2) + // Shift codecs to the end of list if it's not allowed. + var shiftVideoCodecs = new List<string>(); + if (!encodingOptions.AllowHevcEncoding) { - return; + shiftVideoCodecs.Add("hevc"); + shiftVideoCodecs.Add("h265"); + } + + if (!encodingOptions.AllowAv1Encoding) + { + shiftVideoCodecs.Add("av1"); } - var shiftVideoCodecs = new[] { "hevc", "h265" }; if (videoCodecs.All(i => shiftVideoCodecs.Contains(i, StringComparison.OrdinalIgnoreCase))) { return; @@ -6035,6 +6249,12 @@ namespace MediaBrowser.Controller.MediaEncoding audioTranscodeParams.Add("-acodec " + GetAudioEncoder(state)); } + if (GetAudioEncoder(state).StartsWith("pcm_", StringComparison.Ordinal)) + { + audioTranscodeParams.Add(string.Concat("-f ", GetAudioEncoder(state).AsSpan(4))); + audioTranscodeParams.Add("-ar " + state.BaseRequest.AudioBitRate); + } + if (!string.Equals(outputCodec, "opus", StringComparison.OrdinalIgnoreCase)) { // opus only supports specific sampling rates diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index a6b541660..17813559a 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; @@ -367,22 +368,21 @@ namespace MediaBrowser.Controller.MediaEncoding /// <summary> /// Gets the target video range type. /// </summary> - public string TargetVideoRangeType + public VideoRangeType TargetVideoRangeType { get { if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { - return VideoStream?.VideoRangeType; + return VideoStream?.VideoRangeType ?? VideoRangeType.Unknown; } - var requestedRangeType = GetRequestedRangeTypes(ActualOutputVideoCodec).FirstOrDefault(); - if (!string.IsNullOrEmpty(requestedRangeType)) + if (Enum.TryParse(GetRequestedRangeTypes(ActualOutputVideoCodec).FirstOrDefault() ?? "Unknown", true, out VideoRangeType requestedRangeType)) { return requestedRangeType; } - return null; + return VideoRangeType.Unknown; } } diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index f830b9f29..4114dea4f 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -64,8 +64,8 @@ namespace MediaBrowser.Controller.MediaEncoding /// <summary> /// Gets a value indicating whether the configured Vaapi device supports vulkan drm format modifier. /// </summary> - /// <value><c>true</c> if the Vaapi device supports vulkan drm format modifier, <c>false</c> otherwise.</value> - bool IsVaapiDeviceSupportVulkanFmtModifier { get; } + /// <value><c>true</c> if the Vaapi device supports vulkan drm interop, <c>false</c> otherwise.</value> + bool IsVaapiDeviceSupportVulkanDrmInterop { get; } /// <summary> /// Whether given encoder codec is supported. diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index 3b34af4e9..3d288b9f8 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -20,12 +20,12 @@ namespace MediaBrowser.Controller.MediaEncoding _logger = logger; } - public async Task StartStreamingLog(EncodingJobInfo state, Stream source, Stream target) + public async Task StartStreamingLog(EncodingJobInfo state, StreamReader reader, Stream target) { try { using (target) - using (var reader = new StreamReader(source)) + using (reader) { while (!reader.EndOfStream && reader.BaseStream.CanRead) { diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index 0524999c7..e0942e490 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -9,7 +9,7 @@ using System.Linq; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Model.Net; +using MediaBrowser.Controller.Net.WebSocketMessages; using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -96,7 +96,7 @@ namespace MediaBrowser.Controller.Net /// Starts sending messages over a web socket. /// </summary> /// <param name="message">The message.</param> - private void Start(WebSocketMessageInfo message) + protected virtual void Start(WebSocketMessageInfo message) { var vals = message.Data.Split(','); @@ -169,9 +169,8 @@ namespace MediaBrowser.Controller.Net if (data is not null) { await connection.SendAsync( - new WebSocketMessage<TReturnDataType> + new OutboundWebSocketMessage<TReturnDataType> { - MessageId = Guid.NewGuid(), MessageType = Type, Data = data }, diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs index 4f2492b89..bba5a6b85 100644 --- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs +++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs @@ -1,14 +1,15 @@ -#pragma warning disable CS1591 - using System; using System.Net; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Model.Net; +using MediaBrowser.Controller.Net.WebSocketMessages; namespace MediaBrowser.Controller.Net { + /// <summary> + /// Interface for WebSocket connections. + /// </summary> public interface IWebSocketConnection : IAsyncDisposable, IDisposable { /// <summary> @@ -41,6 +42,11 @@ namespace MediaBrowser.Controller.Net WebSocketState State { get; } /// <summary> + /// Gets the authorization information. + /// </summary> + public AuthorizationInfo AuthorizationInfo { get; } + + /// <summary> /// Gets the remote end point. /// </summary> /// <value>The remote end point.</value> @@ -49,13 +55,27 @@ namespace MediaBrowser.Controller.Net /// <summary> /// Sends a message asynchronously. /// </summary> + /// <param name="message">The message.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + /// <exception cref="ArgumentNullException">The message is null.</exception> + Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken); + + /// <summary> + /// Sends a message asynchronously. + /// </summary> /// <typeparam name="T">The type of websocket message data.</typeparam> /// <param name="message">The message.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> /// <exception cref="ArgumentNullException">The message is null.</exception> - Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken); + Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken); - Task ProcessAsync(CancellationToken cancellationToken = default); + /// <summary> + /// Receives a message asynchronously. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task ReceiveAsync(CancellationToken cancellationToken = default); } } diff --git a/MediaBrowser.Controller/Net/WebSocketMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessage.cs new file mode 100644 index 000000000..92183e792 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessage.cs @@ -0,0 +1,22 @@ +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 server id. + /// </summary> + [JsonIgnore] + public string? ServerId { get; set; } +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs index 6f7ebf156..f7a9ccc44 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs @@ -1,13 +1,13 @@ #nullable disable -using MediaBrowser.Model.Net; +using MediaBrowser.Controller.Net.WebSocketMessages; namespace MediaBrowser.Controller.Net { /// <summary> /// Class WebSocketMessageInfo. /// </summary> - public class WebSocketMessageInfo : WebSocketMessage<string> + public class WebSocketMessageInfo : InboundWebSocketMessage<string> { /// <summary> /// Gets or sets the connection. diff --git a/MediaBrowser.Controller/Net/WebSocketMessageOfT.cs b/MediaBrowser.Controller/Net/WebSocketMessageOfT.cs new file mode 100644 index 000000000..11e5a6bb2 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessageOfT.cs @@ -0,0 +1,32 @@ +#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> +public abstract class WebSocketMessage<T> : WebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="WebSocketMessage{T}"/> class. + /// </summary> + protected 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 000000000..c3cf9955a --- /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 000000000..c74a254a6 --- /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 000000000..b3a60199a --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStartMessage.cs @@ -0,0 +1,25 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; + +/// <summary> +/// Activity log entry start message. +/// Data is the timing data encoded as "$initialDelay,$interval" in ms. +/// </summary> +public class ActivityLogEntryStartMessage : InboundWebSocketMessage<string> +{ + /// <summary> + /// Initializes a new instance of the <see cref="ActivityLogEntryStartMessage"/> class. + /// Data is the timing data encoded as "$initialDelay,$interval" in ms. + /// </summary> + /// <param name="data">The timing data encoded as "$initialDelay,$interval".</param> + public ActivityLogEntryStartMessage(string 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 000000000..6f65cb2c7 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStopMessage.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; + +/// <summary> +/// Activity log entry stop message. +/// </summary> +public class ActivityLogEntryStopMessage : InboundWebSocketMessage +{ + /// <inheritdoc /> + [DefaultValue(SessionMessageType.ActivityLogEntryStop)] + public override SessionMessageType MessageType => SessionMessageType.ActivityLogEntryStop; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/InboundKeepAliveMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/InboundKeepAliveMessage.cs new file mode 100644 index 000000000..fec7cb4e4 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/InboundKeepAliveMessage.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; + +/// <summary> +/// Keep alive websocket messages. +/// </summary> +public class InboundKeepAliveMessage : InboundWebSocketMessage +{ + /// <inheritdoc /> + [DefaultValue(SessionMessageType.KeepAlive)] + public override SessionMessageType MessageType => SessionMessageType.KeepAlive; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStartMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStartMessage.cs new file mode 100644 index 000000000..bf98470bf --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStartMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; + +/// <summary> +/// Scheduled tasks info start message. +/// Data is the timing data encoded as "$initialDelay,$interval" in ms. +/// </summary> +public class ScheduledTasksInfoStartMessage : InboundWebSocketMessage<string> +{ + /// <summary> + /// Initializes a new instance of the <see cref="ScheduledTasksInfoStartMessage"/> class. + /// </summary> + /// <param name="data">The timing data encoded as $initialDelay,$interval.</param> + public ScheduledTasksInfoStartMessage(string 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 000000000..f36739c70 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStopMessage.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; + +/// <summary> +/// Scheduled tasks info stop message. +/// </summary> +public class ScheduledTasksInfoStopMessage : InboundWebSocketMessage +{ + /// <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 000000000..a40a0c79e --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStartMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; + +/// <summary> +/// Sessions start message. +/// Data is the timing data encoded as "$initialDelay,$interval" in ms. +/// </summary> +public class SessionsStartMessage : InboundWebSocketMessage<string> +{ + /// <summary> + /// Initializes a new instance of the <see cref="SessionsStartMessage"/> class. + /// </summary> + /// <param name="data">The timing data encoded as $initialDelay,$interval.</param> + public SessionsStartMessage(string 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 000000000..288d111c5 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStopMessage.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; + +/// <summary> +/// Sessions stop message. +/// </summary> +public class SessionsStopMessage : InboundWebSocketMessage +{ + /// <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 000000000..8d6e821df --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessage.cs @@ -0,0 +1,8 @@ +namespace MediaBrowser.Controller.Net.WebSocketMessages; + +/// <summary> +/// Inbound websocket message. +/// </summary> +public class InboundWebSocketMessage : WebSocketMessage, IInboundWebSocketMessage +{ +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessageOfT.cs b/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessageOfT.cs new file mode 100644 index 000000000..4da5e7d31 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessageOfT.cs @@ -0,0 +1,26 @@ +#pragma warning disable SA1649 // File name must equal class name. + +namespace MediaBrowser.Controller.Net.WebSocketMessages; + +/// <summary> +/// Inbound websocket message with data. +/// </summary> +/// <typeparam name="T">The data type.</typeparam> +public class InboundWebSocketMessage<T> : WebSocketMessage<T>, IInboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="InboundWebSocketMessage{T}"/> class. + /// </summary> + public InboundWebSocketMessage() + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="InboundWebSocketMessage{T}"/> class. + /// </summary> + /// <param name="data">The data to send.</param> + protected InboundWebSocketMessage(T data) + { + Data = data; + } +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ActivityLogEntryMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ActivityLogEntryMessage.cs new file mode 100644 index 000000000..2a098615d --- /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 : OutboundWebSocketMessage<IReadOnlyList<ActivityLogEntry>> +{ + /// <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 000000000..ca55340a0 --- /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 : OutboundWebSocketMessage<int> +{ + /// <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 000000000..5fbbb0624 --- /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 : OutboundWebSocketMessage<GeneralCommand> +{ + /// <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 000000000..47417c405 --- /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 : OutboundWebSocketMessage<LibraryUpdateInfo> +{ + /// <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/OutboundKeepAliveMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/OutboundKeepAliveMessage.cs new file mode 100644 index 000000000..d907dcff9 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/OutboundKeepAliveMessage.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Keep alive websocket messages. +/// </summary> +public class OutboundKeepAliveMessage : OutboundWebSocketMessage +{ + /// <inheritdoc /> + [DefaultValue(SessionMessageType.KeepAlive)] + public override SessionMessageType MessageType => SessionMessageType.KeepAlive; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlayMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlayMessage.cs new file mode 100644 index 000000000..86ee2ff90 --- /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 : OutboundWebSocketMessage<PlayRequest> +{ + /// <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 000000000..cd6d28cb3 --- /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 : OutboundWebSocketMessage<PlaystateRequest> +{ + /// <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 000000000..17fd25938 --- /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 : OutboundWebSocketMessage<InstallationInfo> +{ + /// <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 000000000..3e60198ba --- /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 : OutboundWebSocketMessage<InstallationInfo> +{ + /// <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 000000000..40032f16e --- /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 : OutboundWebSocketMessage<InstallationInfo> +{ + /// <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 000000000..28861896f --- /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 : OutboundWebSocketMessage<InstallationInfo> +{ + /// <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 000000000..ca4959119 --- /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 : OutboundWebSocketMessage<PluginInfo> +{ + /// <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 000000000..41b3cd46a --- /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 : OutboundWebSocketMessage<Dictionary<string, string>> +{ + /// <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 000000000..a89f19b61 --- /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 : OutboundWebSocketMessage +{ + /// <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 000000000..afa36fb72 --- /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 : OutboundWebSocketMessage<TaskResult> +{ + /// <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 000000000..c7360779f --- /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 : OutboundWebSocketMessage<IReadOnlyList<TaskInfo>> +{ + /// <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 000000000..f832c8935 --- /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 : OutboundWebSocketMessage<TimerEventInfo> +{ + /// <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 000000000..450b4c799 --- /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 : OutboundWebSocketMessage<TimerEventInfo> +{ + /// <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 000000000..8f09c802f --- /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 : OutboundWebSocketMessage +{ + /// <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 000000000..485e71b6e --- /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 : OutboundWebSocketMessage +{ + /// <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 000000000..3504831b8 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.ComponentModel; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Sessions message. +/// </summary> +public class SessionsMessage : OutboundWebSocketMessage<IReadOnlyList<SessionInfo>> +{ + /// <summary> + /// Initializes a new instance of the <see cref="SessionsMessage"/> class. + /// </summary> + /// <param name="data">Session info.</param> + public SessionsMessage(IReadOnlyList<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 000000000..d0624ec01 --- /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 : OutboundWebSocketMessage<SendCommand> +{ + /// <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 000000000..6a501aa7e --- /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 : OutboundWebSocketMessage<GroupUpdate> +{ + /// <summary> + /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandMessage"/> class. + /// </summary> + /// <param name="data">The send command.</param> + public SyncPlayGroupUpdateCommandMessage(GroupUpdate data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] + public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs new file mode 100644 index 000000000..47f706e2a --- /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 : OutboundWebSocketMessage<GroupUpdate<GroupInfoDto>> +{ + /// <summary> + /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfGroupInfoMessage"/> class. + /// </summary> + /// <param name="data">The group info.</param> + public SyncPlayGroupUpdateCommandOfGroupInfoMessage(GroupUpdate<GroupInfoDto> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] + public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs new file mode 100644 index 000000000..11ddb1e25 --- /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 : OutboundWebSocketMessage<GroupUpdate<GroupStateUpdate>> +{ + /// <summary> + /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage"/> class. + /// </summary> + /// <param name="data">The group info.</param> + public SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage(GroupUpdate<GroupStateUpdate> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] + public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs new file mode 100644 index 000000000..7e73399b1 --- /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 : OutboundWebSocketMessage<GroupUpdate<PlayQueueUpdate>> +{ + /// <summary> + /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage"/> class. + /// </summary> + /// <param name="data">The play queue update.</param> + public SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage(GroupUpdate<PlayQueueUpdate> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] + public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs new file mode 100644 index 000000000..5b5ccd3ed --- /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 : OutboundWebSocketMessage<GroupUpdate<string>> +{ + /// <summary> + /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfStringMessage"/> class. + /// </summary> + /// <param name="data">The send command.</param> + public SyncPlayGroupUpdateCommandOfStringMessage(GroupUpdate<string> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] + public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCancelledMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCancelledMessage.cs new file mode 100644 index 000000000..f44fd126b --- /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 : OutboundWebSocketMessage<TimerEventInfo> +{ + /// <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 000000000..8c1e102eb --- /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 : OutboundWebSocketMessage<TimerEventInfo> +{ + /// <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 000000000..6a053643d --- /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 : OutboundWebSocketMessage<UserDataChangeInfo> +{ + /// <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 000000000..add3f7771 --- /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 : OutboundWebSocketMessage<Guid> +{ + /// <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 000000000..9a72deae1 --- /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 : OutboundWebSocketMessage<UserDto> +{ + /// <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 000000000..178245851 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs @@ -0,0 +1,14 @@ +using System; + +namespace MediaBrowser.Controller.Net.WebSocketMessages; + +/// <summary> +/// Outbound websocket message. +/// </summary> +public class OutboundWebSocketMessage : WebSocketMessage, IOutboundWebSocketMessage +{ + /// <summary> + /// Gets or sets the message id. + /// </summary> + public Guid MessageId { get; set; } = Guid.NewGuid(); +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessageOfT.cs b/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessageOfT.cs new file mode 100644 index 000000000..cce331805 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessageOfT.cs @@ -0,0 +1,33 @@ +#pragma warning disable SA1649 // File name must equal class name. + +using System; + +namespace MediaBrowser.Controller.Net.WebSocketMessages; + +/// <summary> +/// Outbound websocket message with data. +/// </summary> +/// <typeparam name="T">The data type.</typeparam> +public class OutboundWebSocketMessage<T> : WebSocketMessage<T>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="OutboundWebSocketMessage{T}"/> class. + /// </summary> + public OutboundWebSocketMessage() + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="OutboundWebSocketMessage{T}"/> class. + /// </summary> + /// <param name="data">The data to send.</param> + protected OutboundWebSocketMessage(T data) + { + Data = data; + } + + /// <summary> + /// Gets or sets the message id. + /// </summary> + public Guid MessageId { get; set; } = Guid.NewGuid(); +} diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index d88943662..bb68a3b6d 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -44,6 +44,12 @@ namespace MediaBrowser.Controller.Playlists /// <summary> /// Gets the playlists folder. /// </summary> + /// <returns>Folder.</returns> + Folder GetPlaylistsFolder(); + + /// <summary> + /// Gets the playlists folder for a user. + /// </summary> /// <param name="userId">The user identifier.</param> /// <returns>Folder.</returns> Folder GetPlaylistsFolder(Guid userId); @@ -66,10 +72,9 @@ namespace MediaBrowser.Controller.Playlists Task RemovePlaylistsAsync(Guid userId); /// <summary> - /// Updates a playlist. + /// Saves a playlist. /// </summary> - /// <param name="playlist">The updated playlist.</param> - /// <returns>Task.</returns> - Task UpdatePlaylistAsync(Playlist playlist); + /// <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 344e996ea..498df5ab0 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -34,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] @@ -233,6 +236,11 @@ namespace MediaBrowser.Controller.Playlists return base.IsVisible(user); } + if (OpenAccess) + { + return true; + } + var userId = user.Id; if (userId.Equals(OwnerUserId)) { diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs index 7e0a69586..16943f6aa 100644 --- a/MediaBrowser.Controller/Providers/IProviderManager.cs +++ b/MediaBrowser.Controller/Providers/IProviderManager.cs @@ -55,14 +55,6 @@ namespace MediaBrowser.Controller.Providers Task<ItemUpdateType> RefreshSingleItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken); /// <summary> - /// Runs multiple metadata refreshes concurrently. - /// </summary> - /// <param name="action">The action to run.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns> - Task RunMetadataRefresh(Func<Task> action, CancellationToken cancellationToken); - - /// <summary> /// Saves the image. /// </summary> /// <param name="item">The item.</param> @@ -207,15 +199,6 @@ namespace MediaBrowser.Controller.Providers where TItemType : BaseItem, new() where TLookupType : ItemLookupInfo; - /// <summary> - /// Gets the search image. - /// </summary> - /// <param name="providerName">Name of the provider.</param> - /// <param name="url">The URL.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{HttpResponseInfo}.</returns> - Task<HttpResponseMessage> GetSearchImage(string providerName, string url, CancellationToken cancellationToken); - HashSet<Guid> GetRefreshQueue(); void OnRefreshStart(BaseItem item); diff --git a/MediaBrowser.Controller/Resolvers/IItemResolver.cs b/MediaBrowser.Controller/Resolvers/IItemResolver.cs index b95d00aa3..282aa721e 100644 --- a/MediaBrowser.Controller/Resolvers/IItemResolver.cs +++ b/MediaBrowser.Controller/Resolvers/IItemResolver.cs @@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.Resolvers /// </summary> /// <param name="args">The args.</param> /// <returns>BaseItem.</returns> - BaseItem ResolvePath(ItemResolveArgs args); + BaseItem? ResolvePath(ItemResolveArgs args); } public interface IMultiItemResolver diff --git a/MediaBrowser.Controller/Resolvers/ItemResolver.cs b/MediaBrowser.Controller/Resolvers/ItemResolver.cs index a6da8384e..5c9dd6f07 100644 --- a/MediaBrowser.Controller/Resolvers/ItemResolver.cs +++ b/MediaBrowser.Controller/Resolvers/ItemResolver.cs @@ -1,5 +1,3 @@ -#nullable disable - using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -23,7 +21,7 @@ namespace MediaBrowser.Controller.Resolvers /// </summary> /// <param name="args">The args.</param> /// <returns>`0.</returns> - protected internal virtual T Resolve(ItemResolveArgs args) + protected internal virtual T? Resolve(ItemResolveArgs args) { return null; } @@ -42,7 +40,7 @@ namespace MediaBrowser.Controller.Resolvers /// </summary> /// <param name="args">The args.</param> /// <returns>BaseItem.</returns> - public BaseItem ResolvePath(ItemResolveArgs args) + public BaseItem? ResolvePath(ItemResolveArgs args) { var item = Resolve(args); diff --git a/MediaBrowser.Controller/Security/IAuthenticationManager.cs b/MediaBrowser.Controller/Security/IAuthenticationManager.cs index e3d18c8c0..070ab7a85 100644 --- a/MediaBrowser.Controller/Security/IAuthenticationManager.cs +++ b/MediaBrowser.Controller/Security/IAuthenticationManager.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; namespace MediaBrowser.Controller.Security diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 0c4719a0e..53df7133b 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -233,20 +233,6 @@ namespace MediaBrowser.Controller.Session Task SendRestartRequiredNotification(CancellationToken cancellationToken); /// <summary> - /// Sends the server shutdown notification. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - Task SendServerShutdownNotification(CancellationToken cancellationToken); - - /// <summary> - /// Sends the server restart notification. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - Task SendServerRestartNotification(CancellationToken cancellationToken); - - /// <summary> /// Adds the additional user. /// </summary> /// <param name="sessionId">The session identifier.</param> diff --git a/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs b/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs index 85b3e6fbd..51c29c7a2 100644 --- a/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs +++ b/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs @@ -14,6 +14,8 @@ namespace MediaBrowser.Controller.Subtitles public bool IsForced { get; set; } + public bool IsHearingImpaired { get; set; } + public Stream Stream { get; set; } } } diff --git a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs index bdebbbfd4..c0a168192 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); @@ -407,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(); @@ -502,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); } @@ -518,7 +518,7 @@ 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)) { @@ -532,7 +532,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue /// 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) { |
