diff options
95 files changed, 1083 insertions, 479 deletions
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index 1618237f1..c28b1bf7f 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -47,7 +47,7 @@ jobs: displayName: Set release version (stable) condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') - - script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment' + - script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) --label "org.opencontainers.image.url=$(Build.Repository.Uri)" --label "org.opencontainers.image.revision=$(Build.SourceVersion)" deployment' displayName: 'Build Dockerfile' - script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)' diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index f83b38949..eea238189 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1 + uses: github/codeql-action/init@004c5de30b6423267685b897a3d595e944f7fed5 # v2.20.2 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1 + uses: github/codeql-action/autobuild@004c5de30b6423267685b897a3d595e944f7fed5 # v2.20.2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1 + uses: github/codeql-action/analyze@004c5de30b6423267685b897a3d595e944f7fed5 # v2.20.2 diff --git a/Directory.Packages.props b/Directory.Packages.props index 4ea5ab196..0bd7b6e9c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,7 +18,7 @@ <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.2" /> - <PackageVersion Include="FsCheck.Xunit" Version="2.16.5" /> + <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="libse" Version="3.6.13" /> <PackageVersion Include="LrcParser" Version="2023.524.0" /> @@ -46,7 +46,7 @@ <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" /> - <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.2" /> + <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.3" /> <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" /> <PackageVersion Include="MimeTypes" Version="2.4.0" /> <PackageVersion Include="Mono.Nat" Version="3.0.4" /> @@ -65,9 +65,11 @@ <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.1" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> - <PackageVersion Include="SharpFuzz" Version="2.1.0" /> + <PackageVersion Include="SharpFuzz" Version="2.1.1" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" /> <PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" /> + <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.3" /> + <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.3" /> <PackageVersion Include="SkiaSharp" Version="2.88.3" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="SQLitePCL.pretty.netstandard" Version="3.1.0" /> diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index 179683055..b34d0f21e 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -112,7 +112,8 @@ namespace Emby.Server.Implementations.Collections return Path.Combine(_appPaths.DataPath, "collections"); } - private Task<Folder?> GetCollectionsFolder(bool createIfNeeded) + /// <inheritdoc /> + public Task<Folder?> GetCollectionsFolder(bool createIfNeeded) { return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded); } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index ca8f605a0..73ec856fc 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -2452,7 +2452,9 @@ namespace Emby.Server.Implementations.Data if (query.SearchTerm.Length > 1) { builder.Append("+ ((CleanName like @SearchTermContains or (OriginalTitle not null and OriginalTitle like @SearchTermContains)) * 10)"); - builder.Append("+ ((Tags not null and Tags like @SearchTermContains) * 5)"); + builder.Append("+ (SELECT COUNT(1) * 1 from ItemValues where ItemId=Guid and CleanValue like @SearchTermContains)"); + builder.Append("+ (SELECT COUNT(1) * 2 from ItemValues where ItemId=Guid and CleanValue like @SearchTermStartsWith)"); + builder.Append("+ (SELECT COUNT(1) * 10 from ItemValues where ItemId=Guid and CleanValue like @SearchTermEquals)"); } builder.Append(") as SearchScore"); @@ -2483,6 +2485,11 @@ namespace Emby.Server.Implementations.Data { statement.TryBind("@SearchTermContains", "%" + searchTerm + "%"); } + + if (commandText.Contains("@SearchTermEquals", StringComparison.OrdinalIgnoreCase)) + { + statement.TryBind("@SearchTermEquals", searchTerm); + } } private void BindSimilarParams(InternalItemsQuery query, IStatement statement) diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index b1a99853a..fd7653a32 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -9,7 +9,8 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Extensions.Json; using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Net; +using MediaBrowser.Controller.Net.WebSocketMessages; +using MediaBrowser.Controller.Net.WebSocketMessages.Outbound; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; @@ -85,14 +86,15 @@ namespace Emby.Server.Implementations.HttpServer /// <value>The state.</value> public WebSocketState State => _socket.State; - /// <summary> - /// Sends a message asynchronously. - /// </summary> - /// <typeparam name="T">The type of the message.</typeparam> - /// <param name="message">The message.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken) + /// <inheritdoc /> + public Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken) + { + var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions); + return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken); + } + + /// <inheritdoc /> + public Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken) { var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions); return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken); @@ -171,7 +173,7 @@ namespace Emby.Server.Implementations.HttpServer return; } - WebSocketMessage<object>? stub; + InboundWebSocketMessage<object>? stub; long bytesConsumed; try { @@ -212,10 +214,10 @@ namespace Emby.Server.Implementations.HttpServer } } - internal WebSocketMessage<object>? DeserializeWebSocketMessage(ReadOnlySequence<byte> bytes, out long bytesConsumed) + internal InboundWebSocketMessage<object>? DeserializeWebSocketMessage(ReadOnlySequence<byte> bytes, out long bytesConsumed) { var jsonReader = new Utf8JsonReader(bytes); - var ret = JsonSerializer.Deserialize<WebSocketMessage<object>>(ref jsonReader, _jsonOptions); + var ret = JsonSerializer.Deserialize<InboundWebSocketMessage<object>>(ref jsonReader, _jsonOptions); bytesConsumed = jsonReader.BytesConsumed; return ret; } @@ -224,11 +226,7 @@ namespace Emby.Server.Implementations.HttpServer { LastKeepAliveDate = DateTime.UtcNow; return SendAsync( - new WebSocketMessage<string> - { - MessageId = Guid.NewGuid(), - MessageType = SessionMessageType.KeepAlive - }, + new OutboundKeepAliveMessage(), CancellationToken.None); } diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 1fffdfbfa..0ba4a488b 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -20,6 +20,14 @@ namespace Emby.Server.Implementations.IO private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>(); private readonly string _tempPath; private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows(); + private static readonly char[] _invalidPathCharacters = + { + '\"', '<', '>', '|', '\0', + (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, + (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20, + (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, + (char)31, ':', '*', '?', '\\', '/' + }; /// <summary> /// Initializes a new instance of the <see cref="ManagedFileSystem"/> class. @@ -275,8 +283,7 @@ namespace Emby.Server.Implementations.IO /// <exception cref="ArgumentNullException">The filename is null.</exception> public string GetValidFilename(string filename) { - var invalid = Path.GetInvalidFileNameChars(); - var first = filename.IndexOfAny(invalid); + var first = filename.IndexOfAny(_invalidPathCharacters); if (first == -1) { // Fast path for clean strings @@ -285,7 +292,7 @@ namespace Emby.Server.Implementations.IO return string.Create( filename.Length, - (filename, invalid, first), + (filename, _invalidPathCharacters, first), (chars, state) => { state.filename.AsSpan().CopyTo(chars); @@ -293,7 +300,7 @@ namespace Emby.Server.Implementations.IO chars[state.first++] = ' '; var len = chars.Length; - foreach (var c in state.invalid) + foreach (var c in state._invalidPathCharacters) { for (int i = state.first; i < len; i++) { diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index ea45bf0ba..8bb2d3c02 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2069,7 +2069,9 @@ namespace Emby.Server.Implementations.Library .Find(folder => folder is CollectionFolder) as CollectionFolder; } - return collectionFolder is null ? new LibraryOptions() : collectionFolder.GetLibraryOptions(); + return collectionFolder is null + ? new LibraryOptions() + : collectionFolder.GetLibraryOptions(); } public string GetContentType(BaseItem item) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index bcb42e162..acf3964c8 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -30,12 +30,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost { - private static readonly string[] _disallowedSharedStreamExtensions = + private static readonly string[] _disallowedMimeTypes = { - ".mkv", - ".mp4", - ".m3u8", - ".mpd" + "video/x-matroska", + "video/mp4", + "application/vnd.apple.mpegurl", + "application/mpegurl", + "application/x-mpegurl", + "video/vnd.mpeg.dash.mpd" }; private readonly IHttpClientFactory _httpClientFactory; @@ -118,9 +120,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping) { - var extension = Path.GetExtension(mediaSource.Path) ?? string.Empty; + using var message = new HttpRequestMessage(HttpMethod.Head, mediaSource.Path); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .SendAsync(message, cancellationToken) + .ConfigureAwait(false); - if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) + response.EnsureSuccessStatusCode(); + + if (!_disallowedMimeTypes.Contains(response.Content.Headers.ContentType?.ToString(), StringComparison.OrdinalIgnoreCase)) { return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper); } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index e84e1e074..51f46f4da 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -38,7 +38,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts _httpClientFactory = httpClientFactory; _appHost = appHost; OriginalStreamId = originalStreamId; - EnableStreamSharing = true; } public override async Task Open(CancellationToken openCancellationToken) @@ -59,39 +58,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts .GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) .ConfigureAwait(false); - var contentType = response.Content.Headers.ContentType?.ToString() ?? string.Empty; - if (contentType.Contains("matroska", StringComparison.OrdinalIgnoreCase) - || contentType.Contains("mp4", StringComparison.OrdinalIgnoreCase) - || contentType.Contains("dash", StringComparison.OrdinalIgnoreCase) - || contentType.Contains("mpegURL", StringComparison.OrdinalIgnoreCase) - || contentType.Contains("text/", StringComparison.OrdinalIgnoreCase)) - { - // Close the stream without any sharing features - response.Dispose(); - return; - } - - SetTempFilePath("ts"); - var taskCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); _ = StartStreaming(response, taskCompletionSource, LiveStreamCancellationTokenSource.Token); - // OpenedMediaSource.Protocol = MediaProtocol.File; - // OpenedMediaSource.Path = tempFile; - // OpenedMediaSource.ReadAtNativeFramerate = true; - MediaSource.Path = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; MediaSource.Protocol = MediaProtocol.Http; - // OpenedMediaSource.Path = TempFilePath; - // OpenedMediaSource.Protocol = MediaProtocol.File; - - // OpenedMediaSource.Path = _tempFilePath; - // OpenedMediaSource.Protocol = MediaProtocol.File; - // OpenedMediaSource.SupportsDirectPlay = false; - // OpenedMediaSource.SupportsDirectStream = true; - // OpenedMediaSource.SupportsTranscoding = true; var res = await taskCompletionSource.Task.ConfigureAwait(false); if (!res) { @@ -108,15 +81,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts try { Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath); - using var message = response; - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); - await StreamHelper.CopyToAsync( - stream, - fileStream, - IODefaults.CopyToBufferSize, - () => Resolve(openTaskCompletionSource), - cancellationToken).ConfigureAwait(false); + using (response) + { + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); + await StreamHelper.CopyToAsync( + stream, + fileStream, + IODefaults.CopyToBufferSize, + () => Resolve(openTaskCompletionSource), + cancellationToken).ConfigureAwait(false); + } } catch (OperationCanceledException ex) { diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs new file mode 100644 index 000000000..f78fc6f97 --- /dev/null +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Collections; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Tasks; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.ScheduledTasks.Tasks; + +/// <summary> +/// Deletes Path references from collections that no longer exists. +/// </summary> +public class CleanupCollectionPathsTask : IScheduledTask +{ + private readonly ILocalizationManager _localization; + private readonly ICollectionManager _collectionManager; + private readonly ILogger<CleanupCollectionPathsTask> _logger; + private readonly IProviderManager _providerManager; + private readonly IFileSystem _fileSystem; + + /// <summary> + /// Initializes a new instance of the <see cref="CleanupCollectionPathsTask"/> class. + /// </summary> + /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> + /// <param name="collectionManager">Instance of the <see cref="ICollectionManager"/> interface.</param> + /// <param name="logger">The logger.</param> + /// <param name="providerManager">The provider manager.</param> + /// <param name="fileSystem">The filesystem.</param> + public CleanupCollectionPathsTask( + ILocalizationManager localization, + ICollectionManager collectionManager, + ILogger<CleanupCollectionPathsTask> logger, + IProviderManager providerManager, + IFileSystem fileSystem) + { + _localization = localization; + _collectionManager = collectionManager; + _logger = logger; + _providerManager = providerManager; + _fileSystem = fileSystem; + } + + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskCleanCollections"); + + /// <inheritdoc /> + public string Key => "CleanCollections"; + + /// <inheritdoc /> + public string Description => _localization.GetLocalizedString("TaskCleanCollectionsDescription"); + + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + + /// <inheritdoc /> + public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + { + var collectionsFolder = await _collectionManager.GetCollectionsFolder(false).ConfigureAwait(false); + if (collectionsFolder is null) + { + _logger.LogDebug("There is no collection folder to be found"); + return; + } + + var collections = collectionsFolder.Children.OfType<BoxSet>().ToArray(); + _logger.LogDebug("Found {CollectionLength} Boxsets", collections.Length); + + var itemsToRemove = new List<LinkedChild>(); + for (var index = 0; index < collections.Length; index++) + { + var collection = collections[index]; + _logger.LogDebug("Check Boxset {CollectionName}", collection.Name); + + foreach (var collectionLinkedChild in collection.LinkedChildren) + { + if (!File.Exists(collectionLinkedChild.Path)) + { + _logger.LogInformation("Item in boxset {CollectionName} cannot be found at {ItemPath}", collection.Name, collectionLinkedChild.Path); + itemsToRemove.Add(collectionLinkedChild); + } + } + + if (itemsToRemove.Count != 0) + { + _logger.LogDebug("Update Boxset {CollectionName}", collection.Name); + collection.LinkedChildren = collection.LinkedChildren.Except(itemsToRemove).ToArray(); + await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken) + .ConfigureAwait(false); + + _providerManager.QueueRefresh( + collection.Id, + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + ForceSave = true + }, + RefreshPriority.High); + + itemsToRemove.Clear(); + } + + progress.Report(100D / collections.Length * (index + 1)); + } + } + + /// <inheritdoc /> + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + { + return new[] { new TaskTriggerInfo() { Type = TaskTriggerInfo.TriggerStartup } }; + } +} diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index 4e427b1a4..b3c93a904 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -6,9 +6,8 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Extensions; using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Net.WebSocketMessages.Outbound; using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -308,11 +307,7 @@ namespace Emby.Server.Implementations.Session private Task SendForceKeepAlive(IWebSocketConnection webSocket) { return webSocket.SendAsync( - new WebSocketMessage<int> - { - MessageType = SessionMessageType.ForceKeepAlive, - Data = WebSocketLostTimeout - }, + new ForceKeepAliveMessage(WebSocketLostTimeout), CancellationToken.None); } diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs index cdc736950..cf8e0fb00 100644 --- a/Emby.Server.Implementations/Session/WebSocketController.cs +++ b/Emby.Server.Implementations/Session/WebSocketController.cs @@ -7,8 +7,8 @@ using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Net.WebSocketMessages; using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Net; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; @@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Session } return socket.SendAsync( - new WebSocketMessage<T> + new OutboundWebSocketMessage<T> { Data = data, MessageType = name, diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 9f2088e36..ce684e457 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -12,6 +12,7 @@ using Jellyfin.Api.Attributes; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.StreamingDtos; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using Jellyfin.MediaEncoding.Hls.Playlist; using MediaBrowser.Common.Configuration; @@ -1838,7 +1839,7 @@ public class DynamicHlsController : BaseJellyfinApiController || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) { if (EncodingHelper.IsCopyCodec(codec) - && (string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase) + && (state.VideoStream.VideoRangeType == VideoRangeType.DOVI || string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase) || string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase) || string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase))) diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 7650b861f..80128536d 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -256,8 +256,7 @@ public class ItemsController : BaseJellyfinApiController .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); if (includeItemTypes.Length == 1 - && (includeItemTypes[0] == BaseItemKind.Playlist - || includeItemTypes[0] == BaseItemKind.BoxSet)) + && includeItemTypes[0] == BaseItemKind.BoxSet) { parentId = null; } diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index e95edcfd8..63667e7e6 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Extensions; using Jellyfin.Api.Models.StreamingDtos; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; @@ -210,9 +211,9 @@ public class DynamicHlsHelper // Provide SDR HEVC entrance for backward compatibility. if (encodingOptions.AllowHevcEncoding + && !encodingOptions.AllowAv1Encoding && EncodingHelper.IsCopyCodec(state.OutputVideoCodec) - && !string.IsNullOrEmpty(state.VideoStream.VideoRange) - && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase) + && state.VideoStream.VideoRange == VideoRange.HDR && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)) { var requestedVideoProfiles = state.GetRequestedProfiles("hevc"); @@ -252,11 +253,12 @@ public class DynamicHlsHelper // Provide Level 5.0 entrance for backward compatibility. // e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video, // but in fact it is capable of playing videos up to Level 6.1. - if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) + if (encodingOptions.AllowHevcEncoding + && !encodingOptions.AllowAv1Encoding + && EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.VideoStream.Level.HasValue && state.VideoStream.Level > 150 - && !string.IsNullOrEmpty(state.VideoStream.VideoRange) - && string.Equals(state.VideoStream.VideoRange, "SDR", StringComparison.OrdinalIgnoreCase) + && state.VideoStream.VideoRange == VideoRange.SDR && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)) { var playlistCodecsField = new StringBuilder(); @@ -340,17 +342,17 @@ public class DynamicHlsHelper /// <param name="state">StreamState of the current stream.</param> private void AppendPlaylistVideoRangeField(StringBuilder builder, StreamState state) { - if (state.VideoStream is not null && !string.IsNullOrEmpty(state.VideoStream.VideoRange)) + if (state.VideoStream is not null && state.VideoStream.VideoRange != VideoRange.Unknown) { var videoRange = state.VideoStream.VideoRange; if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) { - if (string.Equals(videoRange, "SDR", StringComparison.OrdinalIgnoreCase)) + if (videoRange == VideoRange.SDR) { builder.Append(",VIDEO-RANGE=SDR"); } - if (string.Equals(videoRange, "HDR", StringComparison.OrdinalIgnoreCase)) + if (videoRange == VideoRange.HDR) { builder.Append(",VIDEO-RANGE=PQ"); } @@ -555,6 +557,12 @@ public class DynamicHlsHelper levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120"; levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString); } + + if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)) + { + levelString = state.GetRequestedLevel("av1") ?? "19"; + levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString); + } } if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel)) @@ -566,11 +574,11 @@ public class DynamicHlsHelper } /// <summary> - /// Get the H.26X profile of the output video stream. + /// Get the profile of the output video stream. /// </summary> /// <param name="state">StreamState of the current stream.</param> /// <param name="codec">Video codec.</param> - /// <returns>H.26X profile of the output video stream.</returns> + /// <returns>Profile of the output video stream.</returns> private string GetOutputVideoCodecProfile(StreamState state, string codec) { string profileString = string.Empty; @@ -588,7 +596,8 @@ public class DynamicHlsHelper } if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)) + || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)) { profileString ??= "main"; } @@ -658,9 +667,9 @@ public class DynamicHlsHelper { if (level == 0) { - // This is 0 when there's no requested H.26X level in the device profile - // and the source is not encoded in H.26X - _logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist"); + // This is 0 when there's no requested level in the device profile + // and the source is not encoded in H.26X or AV1 + _logger.LogError("Got invalid level when building CODECS field for HLS master playlist"); return string.Empty; } @@ -677,6 +686,22 @@ public class DynamicHlsHelper return HlsCodecStringHelpers.GetH265String(profile, level); } + if (string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase)) + { + string profile = GetOutputVideoCodecProfile(state, "av1"); + + // Currently we only transcode to 8 bits AV1 + int bitDepth = 8; + if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) + && state.VideoStream != null + && state.VideoStream.BitDepth.HasValue) + { + bitDepth = state.VideoStream.BitDepth.Value; + } + + return HlsCodecStringHelpers.GetAv1String(profile, level, false, bitDepth); + } + return string.Empty; } diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs index 995488397..9a141a16d 100644 --- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs @@ -179,4 +179,62 @@ public static class HlsCodecStringHelpers return result.ToString(); } + + /// <summary> + /// Gets an AV1 codec string. + /// </summary> + /// <param name="profile">AV1 profile.</param> + /// <param name="level">AV1 level.</param> + /// <param name="tierFlag">AV1 tier flag.</param> + /// <param name="bitDepth">AV1 bit depth.</param> + /// <returns>The AV1 codec string.</returns> + public static string GetAv1String(string? profile, int level, bool tierFlag, int bitDepth) + { + // https://aomedia.org/av1/specification/annex-a/ + // FORMAT: [codecTag].[profile].[level][tier].[bitDepth] + StringBuilder result = new StringBuilder("av01", 13); + + if (string.Equals(profile, "Main", StringComparison.OrdinalIgnoreCase)) + { + result.Append(".0"); + } + else if (string.Equals(profile, "High", StringComparison.OrdinalIgnoreCase)) + { + result.Append(".1"); + } + else if (string.Equals(profile, "Professional", StringComparison.OrdinalIgnoreCase)) + { + result.Append(".2"); + } + else + { + // Default to Main + result.Append(".0"); + } + + if (level <= 0 + || level > 31) + { + // Default to the maximum defined level 6.3 + level = 19; + } + + if (bitDepth != 8 + && bitDepth != 10 + && bitDepth != 12) + { + // Default to 8 bits + bitDepth = 8; + } + + result.Append('.') + .Append(level) + .Append(tierFlag ? 'H' : 'M'); + + string bitDepthD2 = bitDepth.ToString("D2", CultureInfo.InvariantCulture); + result.Append('.') + .Append(bitDepthD2); + + return result.ToString(); + } } diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 9c91dcc6f..782cd6568 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -430,12 +430,17 @@ public static class StreamingHelpers { var videoCodec = state.Request.VideoCodec; - if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) || - string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)) { return ".ts"; } + if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase)) + { + return ".mp4"; + } + if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase)) { return ".ogv"; diff --git a/Jellyfin.Data/Enums/VideoRange.cs b/Jellyfin.Data/Enums/VideoRange.cs new file mode 100644 index 000000000..5072e5ba3 --- /dev/null +++ b/Jellyfin.Data/Enums/VideoRange.cs @@ -0,0 +1,22 @@ +namespace Jellyfin.Data.Enums; + +/// <summary> +/// An enum representing video ranges. +/// </summary> +public enum VideoRange +{ + /// <summary> + /// Unknown video range. + /// </summary> + Unknown, + + /// <summary> + /// SDR video range. + /// </summary> + SDR, + + /// <summary> + /// HDR video range. + /// </summary> + HDR +} diff --git a/Jellyfin.Data/Enums/VideoRangeType.cs b/Jellyfin.Data/Enums/VideoRangeType.cs new file mode 100644 index 000000000..7ac7bc20a --- /dev/null +++ b/Jellyfin.Data/Enums/VideoRangeType.cs @@ -0,0 +1,37 @@ +namespace Jellyfin.Data.Enums; + +/// <summary> +/// An enum representing types of video ranges. +/// </summary> +public enum VideoRangeType +{ + /// <summary> + /// Unknown video range type. + /// </summary> + Unknown, + + /// <summary> + /// SDR video range type (8bit). + /// </summary> + SDR, + + /// <summary> + /// HDR10 video range type (10bit). + /// </summary> + HDR10, + + /// <summary> + /// HLG video range type (10bit). + /// </summary> + HLG, + + /// <summary> + /// Dolby Vision video range type (12bit). + /// </summary> + DOVI, + + /// <summary> + /// HDR10+ video range type (10bit to 16bit). + /// </summary> + HDR10Plus +} diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 939376dd8..0c6315c66 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -22,6 +22,7 @@ using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; using MediaBrowser.Model.Activity; +using MediaBrowser.Providers.Lyric; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -93,6 +94,11 @@ namespace Jellyfin.Server serviceCollection.AddSingleton(typeof(ILyricProvider), type); } + foreach (var type in GetExportTypes<ILyricParser>()) + { + serviceCollection.AddSingleton(typeof(ILyricParser), type); + } + base.RegisterServices(serviceCollection); } 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/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/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 b155d674d..e18c1733e 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -46,6 +46,7 @@ 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 static readonly string[] _videoProfilesH264 = new[] { @@ -65,6 +66,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", @@ -113,12 +121,15 @@ namespace MediaBrowser.Controller.MediaEncoding } 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); + + public string GetAv1Encoder(EncodingJobInfo state, EncodingOptions encodingOptions) + => GetH26xOrAv1Encoder("libsvtav1", "av1", state, encodingOptions); - private string GetH264OrH265Encoder(string defaultEncoder, string hwEncoder, EncodingJobInfo state, EncodingOptions 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 @@ -209,8 +220,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; @@ -221,9 +232,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) @@ -235,7 +246,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; } @@ -250,8 +261,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> @@ -266,6 +277,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)) { @@ -565,6 +581,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; } @@ -1204,6 +1225,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)) { @@ -1211,14 +1237,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) @@ -1236,14 +1264,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"; } @@ -1253,7 +1290,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"; } @@ -1391,14 +1428,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; @@ -1413,6 +1454,13 @@ namespace MediaBrowser.Controller.MediaEncoding args += keyFrameArg + gopArg; } + // global_header produced by AMD VA-API encoder causes non-playable fMP4 on iOS + if (codec.Contains("vaapi", StringComparison.OrdinalIgnoreCase) + && _mediaEncoder.IsVaapiDeviceAmd) + { + args += " -flags:v -global_header"; + } + return args; } @@ -1534,18 +1582,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 @@ -1555,7 +1645,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) { @@ -1595,7 +1686,8 @@ 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) { @@ -1622,9 +1714,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 @@ -1755,6 +1853,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) @@ -1822,19 +1928,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)) { @@ -1856,6 +1984,12 @@ 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"; + } + return param; } @@ -1934,12 +2068,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; } @@ -3645,7 +3779,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); @@ -3846,7 +3980,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); @@ -5810,19 +5944,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; 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/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index 0524999c7..8f38d4976 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; @@ -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..79f0846b4 100644 --- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs +++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs @@ -5,7 +5,7 @@ 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 { @@ -49,12 +49,21 @@ 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); } diff --git a/MediaBrowser.Controller/Net/WebSocketMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessage.cs index c02bcd70b..92183e792 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessage.cs @@ -1,4 +1,3 @@ -using System; using System.Text.Json.Serialization; using MediaBrowser.Model.Session; @@ -16,11 +15,6 @@ public abstract class WebSocketMessage public virtual SessionMessageType MessageType { get; set; } /// <summary> - /// Gets or sets the message id. - /// </summary> - public Guid MessageId { get; set; } - - /// <summary> /// Gets or sets the server id. /// </summary> [JsonIgnore] 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 index 7c35c8010..11e5a6bb2 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessageOfT.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessageOfT.cs @@ -6,13 +6,12 @@ namespace MediaBrowser.Controller.Net; /// Class WebSocketMessage. /// </summary> /// <typeparam name="T">The type of the data.</typeparam> -// TODO make this abstract, remove empty ctor. -public class WebSocketMessage<T> : WebSocketMessage +public abstract class WebSocketMessage<T> : WebSocketMessage { /// <summary> /// Initializes a new instance of the <see cref="WebSocketMessage{T}"/> class. /// </summary> - public WebSocketMessage() + protected WebSocketMessage() { } diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStartMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStartMessage.cs index b9f71b922..b3a60199a 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStartMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStartMessage.cs @@ -1,20 +1,20 @@ -using System.Collections.Generic; using System.ComponentModel; -using MediaBrowser.Model.Activity; using MediaBrowser.Model.Session; namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; /// <summary> /// Activity log entry start message. +/// Data is the timing data encoded as "$initialDelay,$interval" in ms. /// </summary> -public class ActivityLogEntryStartMessage : WebSocketMessage<IReadOnlyCollection<ActivityLogEntry>>, IInboundWebSocketMessage +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">Collection of activity log entries.</param> - public ActivityLogEntryStartMessage(IReadOnlyCollection<ActivityLogEntry> data) + /// <param name="data">The timing data encoded as "$initialDelay,$interval".</param> + public ActivityLogEntryStartMessage(string data) : base(data) { } diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStopMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStopMessage.cs index eac129b20..6f65cb2c7 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStopMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStopMessage.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; using System.ComponentModel; -using MediaBrowser.Model.Activity; using MediaBrowser.Model.Session; namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; @@ -8,17 +6,8 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; /// <summary> /// Activity log entry stop message. /// </summary> -public class ActivityLogEntryStopMessage : WebSocketMessage<IReadOnlyCollection<ActivityLogEntry>>, IInboundWebSocketMessage +public class ActivityLogEntryStopMessage : InboundWebSocketMessage { - /// <summary> - /// Initializes a new instance of the <see cref="ActivityLogEntryStopMessage"/> class. - /// </summary> - /// <param name="data">Collection of activity log entries.</param> - public ActivityLogEntryStopMessage(IReadOnlyCollection<ActivityLogEntry> data) - : base(data) - { - } - /// <inheritdoc /> [DefaultValue(SessionMessageType.ActivityLogEntryStop)] public override SessionMessageType MessageType => SessionMessageType.ActivityLogEntryStop; diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/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 index dd2a7145e..bf98470bf 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStartMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStartMessage.cs @@ -1,20 +1,19 @@ -using System.Collections.Generic; using System.ComponentModel; using MediaBrowser.Model.Session; -using MediaBrowser.Model.Tasks; namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; /// <summary> /// Scheduled tasks info start message. +/// Data is the timing data encoded as "$initialDelay,$interval" in ms. /// </summary> -public class ScheduledTasksInfoStartMessage : WebSocketMessage<IReadOnlyCollection<TaskInfo>>, IInboundWebSocketMessage +public class ScheduledTasksInfoStartMessage : InboundWebSocketMessage<string> { /// <summary> /// Initializes a new instance of the <see cref="ScheduledTasksInfoStartMessage"/> class. /// </summary> - /// <param name="data">Collection of task info.</param> - public ScheduledTasksInfoStartMessage(IReadOnlyCollection<TaskInfo> data) + /// <param name="data">The timing data encoded as $initialDelay,$interval.</param> + public ScheduledTasksInfoStartMessage(string data) : base(data) { } diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStopMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStopMessage.cs index 84e1f0166..f36739c70 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStopMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStopMessage.cs @@ -1,24 +1,13 @@ -using System.Collections.Generic; using System.ComponentModel; using MediaBrowser.Model.Session; -using MediaBrowser.Model.Tasks; namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; /// <summary> /// Scheduled tasks info stop message. /// </summary> -public class ScheduledTasksInfoStopMessage : WebSocketMessage<IReadOnlyCollection<TaskInfo>>, IInboundWebSocketMessage +public class ScheduledTasksInfoStopMessage : InboundWebSocketMessage { - /// <summary> - /// Initializes a new instance of the <see cref="ScheduledTasksInfoStopMessage"/> class. - /// </summary> - /// <param name="data">Collection of task info.</param> - public ScheduledTasksInfoStopMessage(IReadOnlyCollection<TaskInfo> data) - : base(data) - { - } - /// <inheritdoc /> [DefaultValue(SessionMessageType.ScheduledTasksInfoStop)] public override SessionMessageType MessageType => SessionMessageType.ScheduledTasksInfoStop; diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStartMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStartMessage.cs index e35a5dc3a..a40a0c79e 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStartMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStartMessage.cs @@ -1,19 +1,19 @@ using System.ComponentModel; -using MediaBrowser.Controller.Session; 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 : WebSocketMessage<SessionInfo>, IInboundWebSocketMessage +public class SessionsStartMessage : InboundWebSocketMessage<string> { /// <summary> /// Initializes a new instance of the <see cref="SessionsStartMessage"/> class. /// </summary> - /// <param name="data">Session info.</param> - public SessionsStartMessage(SessionInfo data) + /// <param name="data">The timing data encoded as $initialDelay,$interval.</param> + public SessionsStartMessage(string data) : base(data) { } diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStopMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStopMessage.cs index 7e3582d64..288d111c5 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStopMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStopMessage.cs @@ -1,5 +1,4 @@ using System.ComponentModel; -using MediaBrowser.Controller.Session; using MediaBrowser.Model.Session; namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; @@ -7,17 +6,8 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; /// <summary> /// Sessions stop message. /// </summary> -public class SessionsStopMessage : WebSocketMessage<SessionInfo>, IInboundWebSocketMessage +public class SessionsStopMessage : InboundWebSocketMessage { - /// <summary> - /// Initializes a new instance of the <see cref="SessionsStopMessage"/> class. - /// </summary> - /// <param name="data">Session info.</param> - public SessionsStopMessage(SessionInfo data) - : base(data) - { - } - /// <inheritdoc /> [DefaultValue(SessionMessageType.SessionsStop)] public override SessionMessageType MessageType => SessionMessageType.SessionsStop; diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessage.cs index 20ca888e1..8d6e821df 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessage.cs @@ -1,9 +1,8 @@ -namespace MediaBrowser.Controller.Net.WebSocketMessages; +namespace MediaBrowser.Controller.Net.WebSocketMessages; /// <summary> -/// Class representing the list of outbound websocket message types. -/// Only used in openapi generation. +/// Inbound websocket message. /// </summary> -public class InboundWebSocketMessage : WebSocketMessage +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 index 5650ee4bb..2a098615d 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ActivityLogEntryMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ActivityLogEntryMessage.cs @@ -8,7 +8,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Activity log created message. /// </summary> -public class ActivityLogEntryMessage : WebSocketMessage<IReadOnlyList<ActivityLogEntry>>, IOutboundWebSocketMessage +public class ActivityLogEntryMessage : OutboundWebSocketMessage<IReadOnlyList<ActivityLogEntry>> { /// <summary> /// Initializes a new instance of the <see cref="ActivityLogEntryMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ForceKeepAliveMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ForceKeepAliveMessage.cs index 94ade5e81..ca55340a0 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ForceKeepAliveMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ForceKeepAliveMessage.cs @@ -6,7 +6,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Force keep alive websocket messages. /// </summary> -public class ForceKeepAliveMessage : WebSocketMessage<int>, IOutboundWebSocketMessage +public class ForceKeepAliveMessage : OutboundWebSocketMessage<int> { /// <summary> /// Initializes a new instance of the <see cref="ForceKeepAliveMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/GeneralCommandMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/GeneralCommandMessage.cs index 6c71e73f9..5fbbb0624 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/GeneralCommandMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/GeneralCommandMessage.cs @@ -6,7 +6,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// General command websocket message. /// </summary> -public class GeneralCommandMessage : WebSocketMessage<GeneralCommand>, IOutboundWebSocketMessage +public class GeneralCommandMessage : OutboundWebSocketMessage<GeneralCommand> { /// <summary> /// Initializes a new instance of the <see cref="GeneralCommandMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/LibraryChangedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/LibraryChangedMessage.cs index 6432ae8ef..47417c405 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/LibraryChangedMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/LibraryChangedMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Library changed message. /// </summary> -public class LibraryChangedMessage : WebSocketMessage<LibraryUpdateInfo>, IOutboundWebSocketMessage +public class LibraryChangedMessage : OutboundWebSocketMessage<LibraryUpdateInfo> { /// <summary> /// Initializes a new instance of the <see cref="LibraryChangedMessage"/> class. 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 index 7f943bda1..86ee2ff90 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlayMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlayMessage.cs @@ -6,7 +6,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Play command websocket message. /// </summary> -public class PlayMessage : WebSocketMessage<PlayRequest>, IOutboundWebSocketMessage +public class PlayMessage : OutboundWebSocketMessage<PlayRequest> { /// <summary> /// Initializes a new instance of the <see cref="PlayMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlaystateMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlaystateMessage.cs index 804ccb37d..cd6d28cb3 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlaystateMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlaystateMessage.cs @@ -6,7 +6,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Playstate message. /// </summary> -public class PlaystateMessage : WebSocketMessage<PlaystateRequest>, IOutboundWebSocketMessage +public class PlaystateMessage : OutboundWebSocketMessage<PlaystateRequest> { /// <summary> /// Initializes a new instance of the <see cref="PlaystateMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCancelledMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCancelledMessage.cs index 3d7dc5c93..17fd25938 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCancelledMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCancelledMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Plugin installation cancelled message. /// </summary> -public class PluginInstallationCancelledMessage : WebSocketMessage<InstallationInfo>, IOutboundWebSocketMessage +public class PluginInstallationCancelledMessage : OutboundWebSocketMessage<InstallationInfo> { /// <summary> /// Initializes a new instance of the <see cref="PluginInstallationCancelledMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCompletedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCompletedMessage.cs index 81268007f..3e60198ba 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCompletedMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCompletedMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Plugin installation completed message. /// </summary> -public class PluginInstallationCompletedMessage : WebSocketMessage<InstallationInfo>, IOutboundWebSocketMessage +public class PluginInstallationCompletedMessage : OutboundWebSocketMessage<InstallationInfo> { /// <summary> /// Initializes a new instance of the <see cref="PluginInstallationCompletedMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationFailedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationFailedMessage.cs index 9177f1293..40032f16e 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationFailedMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationFailedMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Plugin installation failed message. /// </summary> -public class PluginInstallationFailedMessage : WebSocketMessage<InstallationInfo>, IOutboundWebSocketMessage +public class PluginInstallationFailedMessage : OutboundWebSocketMessage<InstallationInfo> { /// <summary> /// Initializes a new instance of the <see cref="PluginInstallationFailedMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallingMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallingMessage.cs index e371440a0..28861896f 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallingMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallingMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Package installing message. /// </summary> -public class PluginInstallingMessage : WebSocketMessage<InstallationInfo>, IOutboundWebSocketMessage +public class PluginInstallingMessage : OutboundWebSocketMessage<InstallationInfo> { /// <summary> /// Initializes a new instance of the <see cref="PluginInstallingMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginUninstalledMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginUninstalledMessage.cs index b2994fc95..ca4959119 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginUninstalledMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginUninstalledMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Plugin uninstalled message. /// </summary> -public class PluginUninstalledMessage : WebSocketMessage<PluginInfo>, IOutboundWebSocketMessage +public class PluginUninstalledMessage : OutboundWebSocketMessage<PluginInfo> { /// <summary> /// Initializes a new instance of the <see cref="PluginUninstalledMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RefreshProgressMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RefreshProgressMessage.cs index 42dbc3029..41b3cd46a 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RefreshProgressMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RefreshProgressMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Refresh progress message. /// </summary> -public class RefreshProgressMessage : WebSocketMessage<Dictionary<string, string>>, IOutboundWebSocketMessage +public class RefreshProgressMessage : OutboundWebSocketMessage<Dictionary<string, string>> { /// <summary> /// Initializes a new instance of the <see cref="RefreshProgressMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RestartRequiredMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RestartRequiredMessage.cs index 3f3d9e4c8..a89f19b61 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RestartRequiredMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RestartRequiredMessage.cs @@ -6,7 +6,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Restart required. /// </summary> -public class RestartRequiredMessage : WebSocketMessage, IOutboundWebSocketMessage +public class RestartRequiredMessage : OutboundWebSocketMessage { /// <inheritdoc /> [DefaultValue(SessionMessageType.RestartRequired)] diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTaskEndedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTaskEndedMessage.cs index d69662b00..afa36fb72 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTaskEndedMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTaskEndedMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Scheduled task ended message. /// </summary> -public class ScheduledTaskEndedMessage : WebSocketMessage<TaskResult>, IOutboundWebSocketMessage +public class ScheduledTaskEndedMessage : OutboundWebSocketMessage<TaskResult> { /// <summary> /// Initializes a new instance of the <see cref="ScheduledTaskEndedMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTasksInfoMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTasksInfoMessage.cs index 41a05b0de..c7360779f 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTasksInfoMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTasksInfoMessage.cs @@ -8,7 +8,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Scheduled tasks info message. /// </summary> -public class ScheduledTasksInfoMessage : WebSocketMessage<IReadOnlyList<TaskInfo>>, IOutboundWebSocketMessage +public class ScheduledTasksInfoMessage : OutboundWebSocketMessage<IReadOnlyList<TaskInfo>> { /// <summary> /// Initializes a new instance of the <see cref="ScheduledTasksInfoMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCancelledMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCancelledMessage.cs index d4950b8b6..f832c8935 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCancelledMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCancelledMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Series timer cancelled message. /// </summary> -public class SeriesTimerCancelledMessage : WebSocketMessage<TimerEventInfo>, IOutboundWebSocketMessage +public class SeriesTimerCancelledMessage : OutboundWebSocketMessage<TimerEventInfo> { /// <summary> /// Initializes a new instance of the <see cref="SeriesTimerCancelledMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCreatedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCreatedMessage.cs index 091c10be6..450b4c799 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCreatedMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCreatedMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Series timer created message. /// </summary> -public class SeriesTimerCreatedMessage : WebSocketMessage<TimerEventInfo>, IOutboundWebSocketMessage +public class SeriesTimerCreatedMessage : OutboundWebSocketMessage<TimerEventInfo> { /// <summary> /// Initializes a new instance of the <see cref="SeriesTimerCreatedMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerRestartingMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerRestartingMessage.cs index a465d8b00..8f09c802f 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerRestartingMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerRestartingMessage.cs @@ -6,7 +6,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Server restarting down message. /// </summary> -public class ServerRestartingMessage : WebSocketMessage, IOutboundWebSocketMessage +public class ServerRestartingMessage : OutboundWebSocketMessage { /// <inheritdoc /> [DefaultValue(SessionMessageType.ServerRestarting)] diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerShuttingDownMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerShuttingDownMessage.cs index 0b998a523..485e71b6e 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerShuttingDownMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerShuttingDownMessage.cs @@ -6,7 +6,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Server shutting down message. /// </summary> -public class ServerShuttingDownMessage : WebSocketMessage, IOutboundWebSocketMessage +public class ServerShuttingDownMessage : OutboundWebSocketMessage { /// <inheritdoc /> [DefaultValue(SessionMessageType.ServerShuttingDown)] diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs index 4c91e0bca..3504831b8 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.ComponentModel; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Session; @@ -7,13 +8,13 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Sessions message. /// </summary> -public class SessionsMessage : WebSocketMessage<SessionInfo>, IOutboundWebSocketMessage +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(SessionInfo data) + public SessionsMessage(IReadOnlyList<SessionInfo> data) : base(data) { } diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayCommandMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayCommandMessage.cs index 17a0fc66e..d0624ec01 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayCommandMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayCommandMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Sync play command. /// </summary> -public class SyncPlayCommandMessage : WebSocketMessage<SendCommand>, IOutboundWebSocketMessage +public class SyncPlayCommandMessage : OutboundWebSocketMessage<SendCommand> { /// <summary> /// Initializes a new instance of the <see cref="SyncPlayCommandMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs index d145d0e01..6a501aa7e 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Untyped sync play command. /// </summary> -public class SyncPlayGroupUpdateCommandMessage : WebSocketMessage<GroupUpdate>, IOutboundWebSocketMessage +public class SyncPlayGroupUpdateCommandMessage : OutboundWebSocketMessage<GroupUpdate> { /// <summary> /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs index 668392c66..47f706e2a 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs @@ -8,7 +8,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// Sync play group update command with group info. /// GroupUpdateTypes: GroupJoined. /// </summary> -public class SyncPlayGroupUpdateCommandOfGroupInfoMessage : WebSocketMessage<GroupUpdate<GroupInfoDto>>, IOutboundWebSocketMessage +public class SyncPlayGroupUpdateCommandOfGroupInfoMessage : OutboundWebSocketMessage<GroupUpdate<GroupInfoDto>> { /// <summary> /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfGroupInfoMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs index ec8c3344f..11ddb1e25 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs @@ -8,7 +8,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// Sync play group update command with group state update. /// GroupUpdateTypes: StateUpdate. /// </summary> -public class SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage : WebSocketMessage<GroupUpdate<GroupStateUpdate>>, IOutboundWebSocketMessage +public class SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage : OutboundWebSocketMessage<GroupUpdate<GroupStateUpdate>> { /// <summary> /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs index 465363f14..7e73399b1 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs @@ -8,7 +8,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// Sync play group update command with play queue update. /// GroupUpdateTypes: PlayQueue. /// </summary> -public class SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage : WebSocketMessage<GroupUpdate<PlayQueueUpdate>>, IOutboundWebSocketMessage +public class SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage : OutboundWebSocketMessage<GroupUpdate<PlayQueueUpdate>> { /// <summary> /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs index b87e9bf71..5b5ccd3ed 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs @@ -8,7 +8,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// Sync play group update command with string. /// GroupUpdateTypes: GroupDoesNotExist (error), LibraryAccessDenied (error), NotInGroup (error), GroupLeft (groupId), UserJoined (username), UserLeft (username). /// </summary> -public class SyncPlayGroupUpdateCommandOfStringMessage : WebSocketMessage<GroupUpdate<string>>, IOutboundWebSocketMessage +public class SyncPlayGroupUpdateCommandOfStringMessage : OutboundWebSocketMessage<GroupUpdate<string>> { /// <summary> /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfStringMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCancelledMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCancelledMessage.cs index 0e70549ef..f44fd126b 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCancelledMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCancelledMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Timer cancelled message. /// </summary> -public class TimerCancelledMessage : WebSocketMessage<TimerEventInfo>, IOutboundWebSocketMessage +public class TimerCancelledMessage : OutboundWebSocketMessage<TimerEventInfo> { /// <summary> /// Initializes a new instance of the <see cref="TimerCancelledMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCreatedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCreatedMessage.cs index 295b3081c..8c1e102eb 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCreatedMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCreatedMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Timer created message. /// </summary> -public class TimerCreatedMessage : WebSocketMessage<TimerEventInfo>, IOutboundWebSocketMessage +public class TimerCreatedMessage : OutboundWebSocketMessage<TimerEventInfo> { /// <summary> /// Initializes a new instance of the <see cref="TimerCreatedMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDataChangedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDataChangedMessage.cs index b60769540..6a053643d 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDataChangedMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDataChangedMessage.cs @@ -6,7 +6,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// User data changed message. /// </summary> -public class UserDataChangedMessage : WebSocketMessage<UserDataChangeInfo>, IOutboundWebSocketMessage +public class UserDataChangedMessage : OutboundWebSocketMessage<UserDataChangeInfo> { /// <summary> /// Initializes a new instance of the <see cref="UserDataChangedMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDeletedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDeletedMessage.cs index 6d527be7f..add3f7771 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDeletedMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDeletedMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// User deleted message. /// </summary> -public class UserDeletedMessage : WebSocketMessage<Guid>, IOutboundWebSocketMessage +public class UserDeletedMessage : OutboundWebSocketMessage<Guid> { /// <summary> /// Initializes a new instance of the <see cref="UserDeletedMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserUpdatedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserUpdatedMessage.cs index 99e9a1f91..9a72deae1 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserUpdatedMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserUpdatedMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// User updated message. /// </summary> -public class UserUpdatedMessage : WebSocketMessage<UserDto>, IOutboundWebSocketMessage +public class UserUpdatedMessage : OutboundWebSocketMessage<UserDto> { /// <summary> /// Initializes a new instance of the <see cref="UserUpdatedMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs index dba3c8392..178245851 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs @@ -1,9 +1,14 @@ +using System; + namespace MediaBrowser.Controller.Net.WebSocketMessages; /// <summary> -/// Class representing the list of outbound websocket message types. -/// Only used in openapi generation. +/// Outbound websocket message. /// </summary> -public class OutboundWebSocketMessage : WebSocketMessage +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/Net/WebSocketMessages/Shared/KeepAliveMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Shared/KeepAliveMessage.cs deleted file mode 100644 index 7f636212c..000000000 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Shared/KeepAliveMessage.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.ComponentModel; -using MediaBrowser.Model.Session; - -namespace MediaBrowser.Controller.Net.WebSocketMessages.Shared; - -/// <summary> -/// Keep alive websocket messages. -/// </summary> -public class KeepAliveMessage : WebSocketMessage<int>, IInboundWebSocketMessage, IOutboundWebSocketMessage -{ - /// <summary> - /// Initializes a new instance of the <see cref="KeepAliveMessage"/> class. - /// </summary> - /// <param name="data">The seconds to keep alive for.</param> - public KeepAliveMessage(int data) - : base(data) - { - } - - /// <inheritdoc /> - [DefaultValue(SessionMessageType.KeepAlive)] - public override SessionMessageType MessageType => SessionMessageType.KeepAlive; -} diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index d3843796f..e1a0e8d67 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -52,6 +52,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { "libx264", "libx265", + "libsvtav1", "mpeg4", "msmpeg4", "libvpx", @@ -69,12 +70,16 @@ namespace MediaBrowser.MediaEncoding.Encoder "srt", "h264_amf", "hevc_amf", + "av1_amf", "h264_qsv", "hevc_qsv", + "av1_qsv", "h264_nvenc", "hevc_nvenc", + "av1_nvenc", "h264_vaapi", "hevc_vaapi", + "av1_vaapi", "h264_v4l2m2m", "h264_videotoolbox", "hevc_videotoolbox" diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index a53be0fee..3f0e98ec8 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -49,6 +49,7 @@ public class EncodingOptions EnableIntelLowPowerHevcHwEncoder = false; EnableHardwareEncoding = true; AllowHevcEncoding = false; + AllowAv1Encoding = false; EnableSubtitleExtraction = true; AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" }; HardwareDecodingCodecs = new string[] { "h264", "vc1" }; @@ -250,6 +251,11 @@ public class EncodingOptions public bool AllowHevcEncoding { get; set; } /// <summary> + /// Gets or sets a value indicating whether AV1 encoding is enabled. + /// </summary> + public bool AllowAv1Encoding { get; set; } + + /// <summary> /// Gets or sets a value indicating whether subtitle extraction is enabled. /// </summary> public bool EnableSubtitleExtraction { get; set; } diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index df6829946..9743edb1c 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs @@ -20,7 +20,6 @@ namespace MediaBrowser.Model.Configuration AutomaticallyAddToCollection = false; EnablePhotos = true; SaveSubtitlesWithMedia = true; - EnableRealtimeMonitor = true; PathInfos = Array.Empty<MediaPathInfo>(); EnableAutomaticSeriesGrouping = true; SeasonZeroDisplayName = "Specials"; diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs index f5e1a3c49..af0787990 100644 --- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs +++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs @@ -1,14 +1,38 @@ -#pragma warning disable CS1591 - using System; using System.Globalization; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Model.Dlna { + /// <summary> + /// The condition processor. + /// </summary> public static class ConditionProcessor { + /// <summary> + /// Checks if a video condition is satisfied. + /// </summary> + /// <param name="condition">The <see cref="ProfileCondition"/>.</param> + /// <param name="width">The width.</param> + /// <param name="height">The height.</param> + /// <param name="videoBitDepth">The bit depth.</param> + /// <param name="videoBitrate">The bitrate.</param> + /// <param name="videoProfile">The video profile.</param> + /// <param name="videoRangeType">The <see cref="VideoRangeType"/>.</param> + /// <param name="videoLevel">The video level.</param> + /// <param name="videoFramerate">The framerate.</param> + /// <param name="packetLength">The packet length.</param> + /// <param name="timestamp">The <see cref="TransportStreamTimestamp"/>.</param> + /// <param name="isAnamorphic">A value indicating whether tthe video is anamorphic.</param> + /// <param name="isInterlaced">A value indicating whether tthe video is interlaced.</param> + /// <param name="refFrames">The reference frames.</param> + /// <param name="numVideoStreams">The number of video streams.</param> + /// <param name="numAudioStreams">The number of audio streams.</param> + /// <param name="videoCodecTag">The video codec tag.</param> + /// <param name="isAvc">A value indicating whether the video is AVC.</param> + /// <returns><b>True</b> if the condition is satisfied.</returns> public static bool IsVideoConditionSatisfied( ProfileCondition condition, int? width, @@ -16,7 +40,7 @@ namespace MediaBrowser.Model.Dlna int? videoBitDepth, int? videoBitrate, string? videoProfile, - string? videoRangeType, + VideoRangeType? videoRangeType, double? videoLevel, float? videoFramerate, int? packetLength, @@ -70,6 +94,13 @@ namespace MediaBrowser.Model.Dlna } } + /// <summary> + /// Checks if a image condition is satisfied. + /// </summary> + /// <param name="condition">The <see cref="ProfileCondition"/>.</param> + /// <param name="width">The width.</param> + /// <param name="height">The height.</param> + /// <returns><b>True</b> if the condition is satisfied.</returns> public static bool IsImageConditionSatisfied(ProfileCondition condition, int? width, int? height) { switch (condition.Property) @@ -83,6 +114,15 @@ namespace MediaBrowser.Model.Dlna } } + /// <summary> + /// Checks if an audio condition is satisfied. + /// </summary> + /// <param name="condition">The <see cref="ProfileCondition"/>.</param> + /// <param name="audioChannels">The channel count.</param> + /// <param name="audioBitrate">The bitrate.</param> + /// <param name="audioSampleRate">The sample rate.</param> + /// <param name="audioBitDepth">The bit depth.</param> + /// <returns><b>True</b> if the condition is satisfied.</returns> public static bool IsAudioConditionSatisfied(ProfileCondition condition, int? audioChannels, int? audioBitrate, int? audioSampleRate, int? audioBitDepth) { switch (condition.Property) @@ -100,6 +140,17 @@ namespace MediaBrowser.Model.Dlna } } + /// <summary> + /// Checks if an audio condition is satisfied for a video. + /// </summary> + /// <param name="condition">The <see cref="ProfileCondition"/>.</param> + /// <param name="audioChannels">The channel count.</param> + /// <param name="audioBitrate">The bitrate.</param> + /// <param name="audioSampleRate">The sample rate.</param> + /// <param name="audioBitDepth">The bit depth.</param> + /// <param name="audioProfile">The profile.</param> + /// <param name="isSecondaryTrack">A value indicating whether the audio is a secondary track.</param> + /// <returns><b>True</b> if the condition is satisfied.</returns> public static bool IsVideoAudioConditionSatisfied( ProfileCondition condition, int? audioChannels, @@ -281,5 +332,41 @@ namespace MediaBrowser.Model.Dlna throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition); } } + + private static bool IsConditionSatisfied(ProfileCondition condition, VideoRangeType? currentValue) + { + if (!currentValue.HasValue || currentValue.Equals(VideoRangeType.Unknown)) + { + // If the value is unknown, it satisfies if not marked as required + return !condition.IsRequired; + } + + var conditionType = condition.Condition; + if (conditionType == ProfileConditionType.EqualsAny) + { + foreach (var singleConditionString in condition.Value.AsSpan().Split('|')) + { + if (Enum.TryParse(singleConditionString, true, out VideoRangeType conditionValue) + && conditionValue.Equals(currentValue)) + { + return true; + } + } + + return false; + } + + if (Enum.TryParse(condition.Value, true, out VideoRangeType expected)) + { + return conditionType switch + { + ProfileConditionType.Equals => currentValue.Value == expected, + ProfileConditionType.NotEquals => currentValue.Value != expected, + _ => throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition) + }; + } + + return false; + } } } diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs index 1d5d0b1de..f29022b54 100644 --- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs +++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using Jellyfin.Data.Enums; using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Model.Dlna @@ -128,7 +129,7 @@ namespace MediaBrowser.Model.Dlna bool isDirectStream, long? runtimeTicks, string videoProfile, - string videoRangeType, + VideoRangeType videoRangeType, double? videoLevel, float? videoFramerate, int? packetLength, diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs index 79ae95170..b7c23669d 100644 --- a/MediaBrowser.Model/Dlna/DeviceProfile.cs +++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel; using System.Xml.Serialization; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Model.MediaInfo; @@ -445,7 +446,7 @@ namespace MediaBrowser.Model.Dlna int? bitDepth, int? videoBitrate, string videoProfile, - string videoRangeType, + VideoRangeType videoRangeType, double? videoLevel, float? videoFramerate, int? packetLength, diff --git a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs index ce422a228..5d7daa81a 100644 --- a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs +++ b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs @@ -73,27 +73,5 @@ namespace MediaBrowser.Model.Dlna return null; } - - private static double GetVideoBitrateScaleFactor(string codec) - { - if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase)) - { - return .6; - } - - return 1; - } - - public static int ScaleBitrate(int bitrate, string inputVideoCodec, string outputVideoCodec) - { - var inputScaleFactor = GetVideoBitrateScaleFactor(inputVideoCodec); - var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec); - var scaleFactor = outputScaleFactor / inputScaleFactor; - var newBitrate = scaleFactor * bitrate; - - return Convert.ToInt32(newBitrate); - } } } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 0a955e917..f6b882c3e 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; @@ -23,7 +24,7 @@ namespace MediaBrowser.Model.Dlna private readonly ILogger _logger; private readonly ITranscoderSupport _transcoderSupport; - private static readonly string[] _supportedHlsVideoCodecs = new string[] { "h264", "hevc" }; + private static readonly string[] _supportedHlsVideoCodecs = new string[] { "h264", "hevc", "av1" }; private static readonly string[] _supportedHlsAudioCodecsTs = new string[] { "aac", "ac3", "eac3", "mp3" }; private static readonly string[] _supportedHlsAudioCodecsMp4 = new string[] { "aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd" }; @@ -889,7 +890,7 @@ namespace MediaBrowser.Model.Dlna int? videoBitrate = videoStream?.BitRate; double? videoLevel = videoStream?.Level; string? videoProfile = videoStream?.Profile; - string? videoRangeType = videoStream?.VideoRangeType; + VideoRangeType? videoRangeType = videoStream?.VideoRangeType; float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; bool? isAnamorphic = videoStream?.IsAnamorphic; bool? isInterlaced = videoStream?.IsInterlaced; @@ -1144,7 +1145,7 @@ namespace MediaBrowser.Model.Dlna int? videoBitrate = videoStream?.BitRate; double? videoLevel = videoStream?.Level; string? videoProfile = videoStream?.Profile; - string? videoRangeType = videoStream?.VideoRangeType; + VideoRangeType? videoRangeType = videoStream?.VideoRangeType; float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; bool? isAnamorphic = videoStream?.IsAnamorphic; bool? isInterlaced = videoStream?.IsInterlaced; @@ -1932,6 +1933,10 @@ namespace MediaBrowser.Model.Dlna { item.SetOption(qualifier, "rangetype", string.Join(',', values)); } + else if (condition.Condition == ProfileConditionType.NotEquals) + { + item.SetOption(qualifier, "rangetype", string.Join(',', Enum.GetNames(typeof(VideoRangeType)).Except(values))); + } else if (condition.Condition == ProfileConditionType.EqualsAny) { var currentValue = item.GetOption(qualifier, "rangetype"); diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index a78a28e13..00543616d 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -281,23 +282,24 @@ namespace MediaBrowser.Model.Dlna /// <summary> /// Gets the target video range type that will be in the output stream. /// </summary> - public string TargetVideoRangeType + public VideoRangeType TargetVideoRangeType { get { if (IsDirectStream) { - return TargetVideoStream?.VideoRangeType; + return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown; } var targetVideoCodecs = TargetVideoCodec; var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec)) + if (!string.IsNullOrEmpty(videoCodec) + && Enum.TryParse(GetOption(videoCodec, "rangetype"), true, out VideoRangeType videoRangeType)) { - return GetOption(videoCodec, "rangetype"); + return videoRangeType; } - return TargetVideoStream?.VideoRangeType; + return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown; } } diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 47341f4e1..34642b83a 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Extensions; @@ -148,7 +149,7 @@ namespace MediaBrowser.Model.Entities /// Gets the video range. /// </summary> /// <value>The video range.</value> - public string VideoRange + public VideoRange VideoRange { get { @@ -162,7 +163,7 @@ namespace MediaBrowser.Model.Entities /// Gets the video range type. /// </summary> /// <value>The video range type.</value> - public string VideoRangeType + public VideoRangeType VideoRangeType { get { @@ -306,9 +307,9 @@ namespace MediaBrowser.Model.Entities attributes.Add(Codec.ToUpperInvariant()); } - if (!string.IsNullOrEmpty(VideoRange)) + if (VideoRange != VideoRange.Unknown) { - attributes.Add(VideoRange.ToUpperInvariant()); + attributes.Add(VideoRange.ToString()); } if (!string.IsNullOrEmpty(Title)) @@ -677,23 +678,23 @@ namespace MediaBrowser.Model.Entities return true; } - public (string VideoRange, string VideoRangeType) GetVideoColorRange() + public (VideoRange VideoRange, VideoRangeType VideoRangeType) GetVideoColorRange() { if (Type != MediaStreamType.Video) { - return (null, null); + return (VideoRange.Unknown, VideoRangeType.Unknown); } var colorTransfer = ColorTransfer; if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)) { - return ("HDR", "HDR10"); + return (VideoRange.HDR, VideoRangeType.HDR10); } if (string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase)) { - return ("HDR", "HLG"); + return (VideoRange.HDR, VideoRangeType.HLG); } var codecTag = CodecTag; @@ -711,10 +712,10 @@ namespace MediaBrowser.Model.Entities || string.Equals(codecTag, "dvhe", StringComparison.OrdinalIgnoreCase) || string.Equals(codecTag, "dav1", StringComparison.OrdinalIgnoreCase)) { - return ("HDR", "DOVI"); + return (VideoRange.HDR, VideoRangeType.DOVI); } - return ("SDR", "SDR"); + return (VideoRange.SDR, VideoRangeType.SDR); } } } diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 80f5e2c37..8354c60ef 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -2,6 +2,7 @@ #pragma warning disable CS1591, CA1819 using System; +using System.ComponentModel; using System.Xml.Serialization; using Jellyfin.Data.Enums; using AccessSchedule = Jellyfin.Data.Entities.AccessSchedule; @@ -79,6 +80,7 @@ namespace MediaBrowser.Model.Users /// Gets or sets a value indicating whether this instance can manage collections. /// </summary> /// <value><c>true</c> if this instance is hidden; otherwise, <c>false</c>.</value> + [DefaultValue(false)] public bool EnableCollectionManagement { get; set; } /// <summary> diff --git a/MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs b/MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs new file mode 100644 index 000000000..ab09f278a --- /dev/null +++ b/MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs @@ -0,0 +1,69 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Jellyfin.Extensions; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Resolvers; + +namespace MediaBrowser.Providers.Lyric; + +/// <inheritdoc /> +public class DefaultLyricProvider : ILyricProvider +{ + private static readonly string[] _lyricExtensions = { ".lrc", ".elrc", ".txt" }; + + /// <inheritdoc /> + public string Name => "DefaultLyricProvider"; + + /// <inheritdoc /> + public ResolverPriority Priority => ResolverPriority.First; + + /// <inheritdoc /> + public bool HasLyrics(BaseItem item) + { + var path = GetLyricsPath(item); + return path is not null; + } + + /// <inheritdoc /> + public async Task<LyricFile?> GetLyrics(BaseItem item) + { + var path = GetLyricsPath(item); + if (path is not null) + { + var content = await File.ReadAllTextAsync(path).ConfigureAwait(false); + if (!string.IsNullOrEmpty(content)) + { + return new LyricFile(path, content); + } + } + + return null; + } + + private string? GetLyricsPath(BaseItem item) + { + // Ensure the path to the item is not null + string? itemDirectoryPath = Path.GetDirectoryName(item.Path); + 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(item.Path)}.*")) + { + if (_lyricExtensions.Contains(Path.GetExtension(lyricFilePath.AsSpan()), StringComparison.OrdinalIgnoreCase)) + { + return lyricFilePath; + } + } + + return null; + } +} diff --git a/MediaBrowser.Controller/Lyrics/ILyricProvider.cs b/MediaBrowser.Providers/Lyric/ILyricProvider.cs index 2a04c6152..27ceba72b 100644 --- a/MediaBrowser.Controller/Lyrics/ILyricProvider.cs +++ b/MediaBrowser.Providers/Lyric/ILyricProvider.cs @@ -1,9 +1,8 @@ -using System.Collections.Generic; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Resolvers; -namespace MediaBrowser.Controller.Lyrics; +namespace MediaBrowser.Providers.Lyric; /// <summary> /// Interface ILyricsProvider. @@ -22,15 +21,16 @@ public interface ILyricProvider ResolverPriority Priority { get; } /// <summary> - /// Gets the supported media types for this provider. + /// Checks if an item has lyrics available. /// </summary> - /// <value>The supported media types.</value> - IReadOnlyCollection<string> SupportedMediaTypes { get; } + /// <param name="item">The media item.</param> + /// <returns>Whether lyrics where found or not.</returns> + bool HasLyrics(BaseItem item); /// <summary> /// Gets the lyrics. /// </summary> /// <param name="item">The media item.</param> /// <returns>A task representing found lyrics.</returns> - Task<LyricResponse?> GetLyrics(BaseItem item); + Task<LyricFile?> GetLyrics(BaseItem item); } diff --git a/MediaBrowser.Providers/Lyric/LrcLyricProvider.cs b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs index 7b108921b..7f1ecd743 100644 --- a/MediaBrowser.Providers/Lyric/LrcLyricProvider.cs +++ b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs @@ -3,34 +3,29 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Threading.Tasks; +using Jellyfin.Extensions; using LrcParser.Model; using LrcParser.Parser; -using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.Resolvers; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Lyric; /// <summary> -/// LRC Lyric Provider. +/// LRC Lyric Parser. /// </summary> -public class LrcLyricProvider : ILyricProvider +public class LrcLyricParser : ILyricParser { - private readonly ILogger<LrcLyricProvider> _logger; - private readonly LyricParser _lrcLyricParser; + private static readonly string[] _supportedMediaTypes = { ".lrc", ".elrc" }; private static readonly string[] _acceptedTimeFormats = { "HH:mm:ss", "H:mm:ss", "mm:ss", "m:ss" }; /// <summary> - /// Initializes a new instance of the <see cref="LrcLyricProvider"/> class. + /// Initializes a new instance of the <see cref="LrcLyricParser"/> class. /// </summary> - /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> - public LrcLyricProvider(ILogger<LrcLyricProvider> logger) + public LrcLyricParser() { - _logger = logger; _lrcLyricParser = new LrcParser.Parser.Lrc.LrcParser(); } @@ -41,37 +36,25 @@ public class LrcLyricProvider : ILyricProvider /// Gets the priority. /// </summary> /// <value>The priority.</value> - public ResolverPriority Priority => ResolverPriority.First; + public ResolverPriority Priority => ResolverPriority.Fourth; /// <inheritdoc /> - public IReadOnlyCollection<string> SupportedMediaTypes { get; } = new[] { "lrc", "elrc" }; - - /// <summary> - /// Opens lyric file for the requested item, and processes it for API return. - /// </summary> - /// <param name="item">The item to to process.</param> - /// <returns>If provider can determine lyrics, returns a <see cref="LyricResponse"/> with or without metadata; otherwise, null.</returns> - public async Task<LyricResponse?> GetLyrics(BaseItem item) + public LyricResponse? ParseLyrics(LyricFile lyrics) { - string? lyricFilePath = this.GetLyricFilePath(item.Path); - - if (string.IsNullOrEmpty(lyricFilePath)) + if (!_supportedMediaTypes.Contains(Path.GetExtension(lyrics.Name.AsSpan()), StringComparison.OrdinalIgnoreCase)) { return null; } - var fileMetaData = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - string lrcFileContent = await File.ReadAllTextAsync(lyricFilePath).ConfigureAwait(false); - Song lyricData; try { - lyricData = _lrcLyricParser.Decode(lrcFileContent); + lyricData = _lrcLyricParser.Decode(lyrics.Content); } - catch (Exception ex) + catch (Exception) { - _logger.LogError(ex, "Error parsing lyric file {LyricFilePath} from {Provider}", lyricFilePath, Name); + // Failed to parse, return null so the next parser will be tried return null; } @@ -84,6 +67,7 @@ public class LrcLyricProvider : ILyricProvider .Select(x => x.Text) .ToList(); + var fileMetaData = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); foreach (string metaDataRow in metaDataRows) { var index = metaDataRow.IndexOf(':', StringComparison.OrdinalIgnoreCase); @@ -130,17 +114,10 @@ public class LrcLyricProvider : ILyricProvider // Map metaData values from LRC file to LyricMetadata properties LyricMetadata lyricMetadata = MapMetadataValues(fileMetaData); - return new LyricResponse - { - Metadata = lyricMetadata, - Lyrics = lyricList - }; + return new LyricResponse { Metadata = lyricMetadata, Lyrics = lyricList }; } - return new LyricResponse - { - Lyrics = lyricList - }; + return new LyricResponse { Lyrics = lyricList }; } /// <summary> diff --git a/MediaBrowser.Providers/Lyric/LyricManager.cs b/MediaBrowser.Providers/Lyric/LyricManager.cs index f9547e0f0..6da811927 100644 --- a/MediaBrowser.Providers/Lyric/LyricManager.cs +++ b/MediaBrowser.Providers/Lyric/LyricManager.cs @@ -12,14 +12,17 @@ namespace MediaBrowser.Providers.Lyric; public class LyricManager : ILyricManager { private readonly ILyricProvider[] _lyricProviders; + private readonly ILyricParser[] _lyricParsers; /// <summary> /// Initializes a new instance of the <see cref="LyricManager"/> class. /// </summary> /// <param name="lyricProviders">All found lyricProviders.</param> - public LyricManager(IEnumerable<ILyricProvider> lyricProviders) + /// <param name="lyricParsers">All found lyricParsers.</param> + public LyricManager(IEnumerable<ILyricProvider> lyricProviders, IEnumerable<ILyricParser> lyricParsers) { _lyricProviders = lyricProviders.OrderBy(i => i.Priority).ToArray(); + _lyricParsers = lyricParsers.OrderBy(i => i.Priority).ToArray(); } /// <inheritdoc /> @@ -27,10 +30,19 @@ public class LyricManager : ILyricManager { foreach (ILyricProvider provider in _lyricProviders) { - var results = await provider.GetLyrics(item).ConfigureAwait(false); - if (results is not null) + var lyrics = await provider.GetLyrics(item).ConfigureAwait(false); + if (lyrics is null) { - return results; + continue; + } + + foreach (ILyricParser parser in _lyricParsers) + { + var result = parser.ParseLyrics(lyrics); + if (result is not null) + { + return result; + } } } @@ -47,7 +59,7 @@ public class LyricManager : ILyricManager continue; } - if (provider.GetLyricFilePath(item.Path) is not null) + if (provider.HasLyrics(item)) { return true; } diff --git a/MediaBrowser.Providers/Lyric/TxtLyricParser.cs b/MediaBrowser.Providers/Lyric/TxtLyricParser.cs new file mode 100644 index 000000000..706f13dbc --- /dev/null +++ b/MediaBrowser.Providers/Lyric/TxtLyricParser.cs @@ -0,0 +1,44 @@ +using System; +using System.IO; +using Jellyfin.Extensions; +using MediaBrowser.Controller.Lyrics; +using MediaBrowser.Controller.Resolvers; + +namespace MediaBrowser.Providers.Lyric; + +/// <summary> +/// TXT Lyric Parser. +/// </summary> +public class TxtLyricParser : ILyricParser +{ + private static readonly string[] _supportedMediaTypes = { ".lrc", ".elrc", ".txt" }; + private static readonly string[] _lineBreakCharacters = { "\r\n", "\r", "\n" }; + + /// <inheritdoc /> + public string Name => "TxtLyricProvider"; + + /// <summary> + /// Gets the priority. + /// </summary> + /// <value>The priority.</value> + public ResolverPriority Priority => ResolverPriority.Fifth; + + /// <inheritdoc /> + public LyricResponse? ParseLyrics(LyricFile lyrics) + { + if (!_supportedMediaTypes.Contains(Path.GetExtension(lyrics.Name.AsSpan()), StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + string[] lyricTextLines = lyrics.Content.Split(_lineBreakCharacters, StringSplitOptions.None); + LyricLine[] lyricList = new LyricLine[lyricTextLines.Length]; + + for (int lyricLineIndex = 0; lyricLineIndex < lyricTextLines.Length; lyricLineIndex++) + { + lyricList[lyricLineIndex] = new LyricLine(lyricTextLines[lyricLineIndex]); + } + + return new LyricResponse { Lyrics = lyricList }; + } +} diff --git a/MediaBrowser.Providers/Lyric/TxtLyricProvider.cs b/MediaBrowser.Providers/Lyric/TxtLyricProvider.cs deleted file mode 100644 index a9099d192..000000000 --- a/MediaBrowser.Providers/Lyric/TxtLyricProvider.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Lyrics; -using MediaBrowser.Controller.Resolvers; - -namespace MediaBrowser.Providers.Lyric; - -/// <summary> -/// TXT Lyric Provider. -/// </summary> -public class TxtLyricProvider : ILyricProvider -{ - /// <inheritdoc /> - public string Name => "TxtLyricProvider"; - - /// <summary> - /// Gets the priority. - /// </summary> - /// <value>The priority.</value> - public ResolverPriority Priority => ResolverPriority.Second; - - /// <inheritdoc /> - public IReadOnlyCollection<string> SupportedMediaTypes { get; } = new[] { "lrc", "elrc", "txt" }; - - /// <summary> - /// Opens lyric file for the requested item, and processes it for API return. - /// </summary> - /// <param name="item">The item to to process.</param> - /// <returns>If provider can determine lyrics, returns a <see cref="LyricResponse"/>; otherwise, null.</returns> - public async Task<LyricResponse?> GetLyrics(BaseItem item) - { - string? lyricFilePath = this.GetLyricFilePath(item.Path); - - if (string.IsNullOrEmpty(lyricFilePath)) - { - return null; - } - - string[] lyricTextLines = await File.ReadAllLinesAsync(lyricFilePath).ConfigureAwait(false); - - if (lyricTextLines.Length == 0) - { - return null; - } - - LyricLine[] lyricList = new LyricLine[lyricTextLines.Length]; - - for (int lyricLineIndex = 0; lyricLineIndex < lyricTextLines.Length; lyricLineIndex++) - { - lyricList[lyricLineIndex] = new LyricLine(lyricTextLines[lyricLineIndex]); - } - - return new LyricResponse - { - Lyrics = lyricList - }; - } -} diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index 3b0333299..034691322 100644 --- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -21,6 +21,8 @@ <PackageReference Include="SkiaSharp" /> <PackageReference Include="SkiaSharp.NativeAssets.Linux" /> <PackageReference Include="SkiaSharp.Svg" /> + <PackageReference Include="SkiaSharp.HarfBuzz" /> + <PackageReference Include="HarfBuzzSharp.NativeAssets.Linux" /> </ItemGroup> <ItemGroup> diff --git a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index eee24c423..a7a3338df 100644 --- a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -3,13 +3,14 @@ using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using SkiaSharp; +using SkiaSharp.HarfBuzz; namespace Jellyfin.Drawing.Skia; /// <summary> /// Used to build collages of multiple images arranged in vertical strips. /// </summary> -public class StripCollageBuilder +public partial class StripCollageBuilder { private readonly SkiaEncoder _skiaEncoder; @@ -22,6 +23,9 @@ public class StripCollageBuilder _skiaEncoder = skiaEncoder; } + [GeneratedRegex(@"\p{IsArabic}|\p{IsArmenian}|\p{IsHebrew}|\p{IsSyriac}|\p{IsThaana}")] + private static partial Regex IsRtlTextRegex(); + /// <summary> /// Check which format an image has been encoded with using its filename extension. /// </summary> @@ -144,7 +148,19 @@ public class StripCollageBuilder textPaint.TextSize = 0.9f * width * textPaint.TextSize / textWidth; } - canvas.DrawText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint); + if (string.IsNullOrWhiteSpace(libraryName)) + { + return bitmap; + } + + if (IsRtlTextRegex().IsMatch(libraryName)) + { + canvas.DrawShapedText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint); + } + else + { + canvas.DrawText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint); + } return bitmap; } |
