From c56dbc1c4410e1b0ec31ca901809b6f627bbb6ed Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sat, 7 Sep 2024 19:23:48 +0200 Subject: Enhance Trickplay (#11883) --- .../Trickplay/TrickplayManager.cs | 183 +++++++++++++++++---- 1 file changed, 155 insertions(+), 28 deletions(-) (limited to 'Jellyfin.Server.Implementations') diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index bb32b7c20e..861037c1fe 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -76,7 +76,65 @@ public class TrickplayManager : ITrickplayManager } /// - public async Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken) + public async Task MoveGeneratedTrickplayDataAsync(Video video, LibraryOptions? libraryOptions, CancellationToken cancellationToken) + { + var options = _config.Configuration.TrickplayOptions; + if (!CanGenerateTrickplay(video, options.Interval)) + { + return; + } + + var existingTrickplayResolutions = await GetTrickplayResolutions(video.Id).ConfigureAwait(false); + foreach (var resolution in existingTrickplayResolutions) + { + cancellationToken.ThrowIfCancellationRequested(); + var existingResolution = resolution.Key; + var tileWidth = resolution.Value.TileWidth; + var tileHeight = resolution.Value.TileHeight; + var shouldBeSavedWithMedia = libraryOptions is null ? false : libraryOptions.SaveTrickplayWithMedia; + var localOutputDir = GetTrickplayDirectory(video, tileWidth, tileHeight, existingResolution, false); + var mediaOutputDir = GetTrickplayDirectory(video, tileWidth, tileHeight, existingResolution, true); + if (shouldBeSavedWithMedia && Directory.Exists(localOutputDir)) + { + var localDirFiles = Directory.GetFiles(localOutputDir); + var mediaDirExists = Directory.Exists(mediaOutputDir); + if (localDirFiles.Length > 0 && ((mediaDirExists && Directory.GetFiles(mediaOutputDir).Length == 0) || !mediaDirExists)) + { + // Move images from local dir to media dir + MoveContent(localOutputDir, mediaOutputDir); + _logger.LogInformation("Moved trickplay images for {ItemName} to {Location}", video.Name, mediaOutputDir); + } + } + else if (Directory.Exists(mediaOutputDir)) + { + var mediaDirFiles = Directory.GetFiles(mediaOutputDir); + var localDirExists = Directory.Exists(localOutputDir); + if (mediaDirFiles.Length > 0 && ((localDirExists && Directory.GetFiles(localOutputDir).Length == 0) || !localDirExists)) + { + // Move images from media dir to local dir + MoveContent(mediaOutputDir, localOutputDir); + _logger.LogInformation("Moved trickplay images for {ItemName} to {Location}", video.Name, localOutputDir); + } + } + } + } + + private void MoveContent(string sourceFolder, string destinationFolder) + { + _fileSystem.MoveDirectory(sourceFolder, destinationFolder); + var parent = Directory.GetParent(sourceFolder); + if (parent is not null) + { + var parentContent = Directory.GetDirectories(parent.FullName); + if (parentContent.Length == 0) + { + Directory.Delete(parent.FullName); + } + } + } + + /// + public async Task RefreshTrickplayDataAsync(Video video, bool replace, LibraryOptions? libraryOptions, CancellationToken cancellationToken) { _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace); @@ -95,6 +153,7 @@ public class TrickplayManager : ITrickplayManager replace, width, options, + libraryOptions, cancellationToken).ConfigureAwait(false); } } @@ -104,6 +163,7 @@ public class TrickplayManager : ITrickplayManager bool replace, int width, TrickplayOptions options, + LibraryOptions? libraryOptions, CancellationToken cancellationToken) { if (!CanGenerateTrickplay(video, options.Interval)) @@ -144,14 +204,53 @@ public class TrickplayManager : ITrickplayManager actualWidth = 2 * ((int)mediaSource.VideoStream.Width / 2); } - var outputDir = GetTrickplayDirectory(video, actualWidth); + var tileWidth = options.TileWidth; + var tileHeight = options.TileHeight; + var saveWithMedia = libraryOptions is null ? false : libraryOptions.SaveTrickplayWithMedia; + var outputDir = GetTrickplayDirectory(video, tileWidth, tileHeight, actualWidth, saveWithMedia); - if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(actualWidth)) + // Import existing trickplay tiles + if (!replace && Directory.Exists(outputDir)) { - _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting", video.Id); - return; + var existingFiles = Directory.GetFiles(outputDir); + if (existingFiles.Length > 0) + { + var hasTrickplayResolution = await HasTrickplayResolutionAsync(video.Id, actualWidth).ConfigureAwait(false); + if (hasTrickplayResolution) + { + _logger.LogDebug("Found existing trickplay files for {ItemId}.", video.Id); + return; + } + + // Import tiles + var localTrickplayInfo = new TrickplayInfo + { + ItemId = video.Id, + Width = width, + Interval = options.Interval, + TileWidth = options.TileWidth, + TileHeight = options.TileHeight, + ThumbnailCount = existingFiles.Length, + Height = 0, + Bandwidth = 0 + }; + + foreach (var tile in existingFiles) + { + var image = _imageEncoder.GetImageSize(tile); + localTrickplayInfo.Height = Math.Max(localTrickplayInfo.Height, image.Height); + var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tile).Length * 8 / localTrickplayInfo.TileWidth / localTrickplayInfo.TileHeight / (localTrickplayInfo.Interval / 1000)); + localTrickplayInfo.Bandwidth = Math.Max(localTrickplayInfo.Bandwidth, bitrate); + } + + await SaveTrickplayInfo(localTrickplayInfo).ConfigureAwait(false); + + _logger.LogDebug("Imported existing trickplay files for {ItemId}.", video.Id); + return; + } } + // Generate trickplay tiles var mediaStream = mediaSource.VideoStream; var container = mediaSource.Container; @@ -224,7 +323,7 @@ public class TrickplayManager : ITrickplayManager } /// - public TrickplayInfo CreateTiles(List images, int width, TrickplayOptions options, string outputDir) + public TrickplayInfo CreateTiles(IReadOnlyList images, int width, TrickplayOptions options, string outputDir) { if (images.Count == 0) { @@ -264,7 +363,7 @@ public class TrickplayManager : ITrickplayManager var tilePath = Path.Combine(workDir, $"{i}.jpg"); imageOptions.OutputPath = tilePath; - imageOptions.InputPaths = images.GetRange(i * thumbnailsPerTile, Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile))); + imageOptions.InputPaths = images.Skip(i * thumbnailsPerTile).Take(Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile))).ToList(); // Generate image and use returned height for tiles info var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null); @@ -289,7 +388,7 @@ public class TrickplayManager : ITrickplayManager Directory.Delete(outputDir, true); } - MoveDirectory(workDir, outputDir); + _fileSystem.MoveDirectory(workDir, outputDir); return trickplayInfo; } @@ -355,6 +454,24 @@ public class TrickplayManager : ITrickplayManager return trickplayResolutions; } + /// + public async Task> GetTrickplayItemsAsync() + { + List trickplayItems; + + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + trickplayItems = await dbContext.TrickplayInfos + .AsNoTracking() + .Select(i => i.ItemId) + .ToListAsync() + .ConfigureAwait(false); + } + + return trickplayItems; + } + /// public async Task SaveTrickplayInfo(TrickplayInfo info) { @@ -392,9 +509,15 @@ public class TrickplayManager : ITrickplayManager } /// - public string GetTrickplayTilePath(BaseItem item, int width, int index) + public async Task GetTrickplayTilePathAsync(BaseItem item, int width, int index, bool saveWithMedia) { - return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg"); + var trickplayResolutions = await GetTrickplayResolutions(item.Id).ConfigureAwait(false); + if (trickplayResolutions is not null && trickplayResolutions.TryGetValue(width, out var trickplayInfo)) + { + return Path.Combine(GetTrickplayDirectory(item, trickplayInfo.TileWidth, trickplayInfo.TileHeight, width, saveWithMedia), index + ".jpg"); + } + + return string.Empty; } /// @@ -470,29 +593,33 @@ public class TrickplayManager : ITrickplayManager return null; } - private string GetTrickplayDirectory(BaseItem item, int? width = null) + /// + public string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false) { - var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay"); - - return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path; + var path = saveWithMedia + ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay")) + : Path.Combine(item.GetInternalMetadataPath(), "trickplay"); + + var subdirectory = string.Format( + CultureInfo.InvariantCulture, + "{0} - {1}x{2}", + width.ToString(CultureInfo.InvariantCulture), + tileWidth.ToString(CultureInfo.InvariantCulture), + tileHeight.ToString(CultureInfo.InvariantCulture)); + + return Path.Combine(path, subdirectory); } - private void MoveDirectory(string source, string destination) + private async Task HasTrickplayResolutionAsync(Guid itemId, int width) { - try - { - Directory.Move(source, destination); - } - catch (IOException) + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { - // Cross device move requires a copy - Directory.CreateDirectory(destination); - foreach (string file in Directory.GetFiles(source)) - { - File.Copy(file, Path.Join(destination, Path.GetFileName(file)), true); - } - - Directory.Delete(source, true); + return await dbContext.TrickplayInfos + .AsNoTracking() + .Where(i => i.ItemId.Equals(itemId)) + .AnyAsync(i => i.Width == width) + .ConfigureAwait(false); } } } -- cgit v1.2.3 From 5ceedced1c4a8bac5b5b7a5f2bd0913783bd427b Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 7 Sep 2024 22:56:51 +0200 Subject: Feature/media segments plugin api (#12359) --- CONTRIBUTORS.md | 1 + .../Localization/Core/en-US.json | 2 + .../Tasks/MediaSegmentExtractionTask.cs | 118 +++++++++++++++++++++ .../MediaSegments/MediaSegmentManager.cs | 102 +++++++++++++++++- .../MediaSegements/IMediaSegmentManager.cs | 17 +++ .../MediaSegements/IMediaSegmentProvider.cs | 36 +++++++ MediaBrowser.Model/Configuration/LibraryOptions.cs | 6 ++ .../Configuration/MetadataPluginType.cs | 3 +- .../MediaSegments/MediaSegmentGenerationRequest.cs | 14 +++ MediaBrowser.Providers/Manager/ProviderManager.cs | 15 ++- .../Manager/ProviderManagerTests.cs | 3 +- 11 files changed, 312 insertions(+), 5 deletions(-) create mode 100644 Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs create mode 100644 MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs create mode 100644 MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs (limited to 'Jellyfin.Server.Implementations') diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index cdf8df17fc..91faa2c2e1 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -65,6 +65,7 @@ - [joshuaboniface](https://github.com/joshuaboniface) - [JustAMan](https://github.com/JustAMan) - [justinfenn](https://github.com/justinfenn) + - [JPVenson](https://github.com/JPVenson) - [KerryRJ](https://github.com/KerryRJ) - [Larvitar](https://github.com/Larvitar) - [LeoVerto](https://github.com/LeoVerto) diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json index d248fc303b..9702ab7123 100644 --- a/Emby.Server.Implementations/Localization/Core/en-US.json +++ b/Emby.Server.Implementations/Localization/Core/en-US.json @@ -132,6 +132,8 @@ "TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.", "TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists", "TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist.", + "TaskExtractMediaSegments": "Media Segment Scan", + "TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.", "TaskMoveTrickplayImages": "Migrate Trickplay Image Location", "TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings." } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs new file mode 100644 index 0000000000..d6fad7526b --- /dev/null +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Tasks; + +namespace Emby.Server.Implementations.ScheduledTasks.Tasks; + +/// +/// Task to obtain media segments. +/// +public class MediaSegmentExtractionTask : IScheduledTask +{ + /// + /// The library manager. + /// + private readonly ILibraryManager _libraryManager; + private readonly ILocalizationManager _localization; + private readonly IMediaSegmentManager _mediaSegmentManager; + private static readonly BaseItemKind[] _itemTypes = [BaseItemKind.Episode, BaseItemKind.Movie, BaseItemKind.Audio, BaseItemKind.AudioBook]; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The localization manager. + /// The segment manager. + public MediaSegmentExtractionTask(ILibraryManager libraryManager, ILocalizationManager localization, IMediaSegmentManager mediaSegmentManager) + { + _libraryManager = libraryManager; + _localization = localization; + _mediaSegmentManager = mediaSegmentManager; + } + + /// + public string Name => _localization.GetLocalizedString("TaskExtractMediaSegments"); + + /// + public string Description => _localization.GetLocalizedString("TaskExtractMediaSegmentsDescription"); + + /// + public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); + + /// + public string Key => "TaskExtractMediaSegments"; + + /// + public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + progress.Report(0); + + var pagesize = 100; + + var query = new InternalItemsQuery + { + MediaTypes = new[] { MediaType.Video, MediaType.Audio }, + IsVirtualItem = false, + IncludeItemTypes = _itemTypes, + DtoOptions = new DtoOptions(true), + SourceTypes = new[] { SourceType.Library }, + Recursive = true, + Limit = pagesize + }; + + var numberOfVideos = _libraryManager.GetCount(query); + + var startIndex = 0; + var numComplete = 0; + + while (startIndex < numberOfVideos) + { + query.StartIndex = startIndex; + + var baseItems = _libraryManager.GetItemList(query); + var currentPageCount = baseItems.Count; + // TODO parallelize with Parallel.ForEach? + for (var i = 0; i < currentPageCount; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + var item = baseItems[i]; + // Only local files supported + if (item.IsFileProtocol && File.Exists(item.Path)) + { + await _mediaSegmentManager.RunSegmentPluginProviders(item, false, cancellationToken).ConfigureAwait(false); + } + + // Update progress + numComplete++; + double percent = (double)numComplete / numberOfVideos; + progress.Report(100 * percent); + } + + startIndex += pagesize; + } + + progress.Report(100); + } + + /// + public IEnumerable GetDefaultTriggers() + { + yield return new TaskTriggerInfo + { + Type = TaskTriggerInfo.TriggerInterval, + IntervalTicks = TimeSpan.FromHours(12).Ticks + }; + } +} diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs index 7916d15c92..9953c05bee 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs @@ -1,14 +1,23 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Globalization; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model; using MediaBrowser.Model.MediaSegments; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Implementations.MediaSegments; @@ -17,15 +26,89 @@ namespace Jellyfin.Server.Implementations.MediaSegments; /// public class MediaSegmentManager : IMediaSegmentManager { + private readonly ILogger _logger; private readonly IDbContextFactory _dbProvider; + private readonly IMediaSegmentProvider[] _segmentProviders; + private readonly ILibraryManager _libraryManager; /// /// Initializes a new instance of the class. /// + /// Logger. /// EFCore Database factory. - public MediaSegmentManager(IDbContextFactory dbProvider) + /// List of all media segment providers. + /// Library manager. + public MediaSegmentManager( + ILogger logger, + IDbContextFactory dbProvider, + IEnumerable segmentProviders, + ILibraryManager libraryManager) { + _logger = logger; _dbProvider = dbProvider; + + _segmentProviders = segmentProviders + .OrderBy(i => i is IHasOrder hasOrder ? hasOrder.Order : 0) + .ToArray(); + _libraryManager = libraryManager; + } + + /// + public async Task RunSegmentPluginProviders(BaseItem baseItem, bool overwrite, CancellationToken cancellationToken) + { + var libraryOptions = _libraryManager.GetLibraryOptions(baseItem); + var providers = _segmentProviders + .Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name))) + .OrderBy(i => + { + var index = libraryOptions.MediaSegmentProvideOrder.IndexOf(i.Name); + return index == -1 ? int.MaxValue : index; + }) + .ToList(); + + _logger.LogInformation("Start media segment extraction from providers with {CountProviders} enabled", providers.Count); + using var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + + if (!overwrite && (await db.MediaSegments.AnyAsync(e => e.ItemId.Equals(baseItem.Id), cancellationToken).ConfigureAwait(false))) + { + _logger.LogInformation("Skip {MediaPath} as it already contains media segments", baseItem.Path); + return; + } + + _logger.LogInformation("Clear existing Segments for {MediaPath}", baseItem.Path); + + await db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + + // no need to recreate the request object every time. + var requestItem = new MediaSegmentGenerationRequest() { ItemId = baseItem.Id }; + + foreach (var provider in providers) + { + if (!await provider.Supports(baseItem).ConfigureAwait(false)) + { + _logger.LogDebug("Media Segment provider {ProviderName} does not support item with path {Path}", provider.Name, baseItem.Path); + continue; + } + + _logger.LogDebug("Run Media Segment provider {ProviderName}", provider.Name); + try + { + var segments = await provider.GetMediaSegments(requestItem, cancellationToken) + .ConfigureAwait(false); + + _logger.LogInformation("Media Segment provider {ProviderName} found {CountSegments} for {MediaPath}", provider.Name, segments.Count, baseItem.Path); + var providerId = GetProviderId(provider.Name); + foreach (var segment in segments) + { + segment.ItemId = baseItem.Id; + await CreateSegmentAsync(segment, providerId).ConfigureAwait(false); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Provider {ProviderName} failed to extract segments from {MediaPath}", provider.Name, baseItem.Path); + } + } } /// @@ -103,4 +186,21 @@ public class MediaSegmentManager : IMediaSegmentManager { return baseItem.MediaType is Data.Enums.MediaType.Video or Data.Enums.MediaType.Audio; } + + /// + public IEnumerable<(string Name, string Id)> GetSupportedProviders(BaseItem item) + { + if (item is not (Video or Audio)) + { + return []; + } + + return _segmentProviders + .Select(p => (p.Name, GetProviderId(p.Name))); + } + + private string GetProviderId(string name) + => name.ToLowerInvariant() + .GetMD5() + .ToString("N", CultureInfo.InvariantCulture); } diff --git a/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs b/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs index 67384f6f64..010d7edb4f 100644 --- a/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs +++ b/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; @@ -13,6 +14,15 @@ namespace MediaBrowser.Controller; /// public interface IMediaSegmentManager { + /// + /// Uses all segment providers enabled for the 's library to get the Media Segments. + /// + /// The Item to evaluate. + /// If set, will remove existing segments and replace it with new ones otherwise will check for existing segments and if found any, stops. + /// stop request token. + /// A task that indicates the Operation is finished. + Task RunSegmentPluginProviders(BaseItem baseItem, bool overwrite, CancellationToken cancellationToken); + /// /// Returns if this item supports media segments. /// @@ -50,4 +60,11 @@ public interface IMediaSegmentManager /// True if there are any segments stored for the item, otherwise false. /// TODO: this should be async but as the only caller BaseItem.GetVersionInfo isn't async, this is also not. Venson. bool HasSegments(Guid itemId); + + /// + /// Gets a list of all registered Segment Providers and their IDs. + /// + /// The media item that should be tested for providers. + /// A list of all providers for the tested item. + IEnumerable<(string Name, string Id)> GetSupportedProviders(BaseItem item); } diff --git a/MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs b/MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs new file mode 100644 index 0000000000..39bb58bef2 --- /dev/null +++ b/MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model; +using MediaBrowser.Model.MediaSegments; + +namespace MediaBrowser.Controller; + +/// +/// Provides methods for Obtaining the Media Segments from an Item. +/// +public interface IMediaSegmentProvider +{ + /// + /// Gets the provider name. + /// + string Name { get; } + + /// + /// Enumerates all Media Segments from an Media Item. + /// + /// Arguments to enumerate MediaSegments. + /// Abort token. + /// A list of all MediaSegments found from this provider. + Task> GetMediaSegments(MediaSegmentGenerationRequest request, CancellationToken cancellationToken); + + /// + /// Should return support state for the given item. + /// + /// The base item to extract segments from. + /// True if item is supported, otherwise false. + ValueTask Supports(BaseItem item); +} diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index 688a6418d0..90ac377f47 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs @@ -11,6 +11,8 @@ namespace MediaBrowser.Model.Configuration { TypeOptions = Array.Empty(); DisabledSubtitleFetchers = Array.Empty(); + DisabledMediaSegmentProviders = Array.Empty(); + MediaSegmentProvideOrder = Array.Empty(); SubtitleFetcherOrder = Array.Empty(); DisabledLocalMetadataReaders = Array.Empty(); DisabledLyricFetchers = Array.Empty(); @@ -87,6 +89,10 @@ namespace MediaBrowser.Model.Configuration public string[] SubtitleFetcherOrder { get; set; } + public string[] DisabledMediaSegmentProviders { get; set; } + + public string[] MediaSegmentProvideOrder { get; set; } + public bool SkipSubtitlesIfEmbeddedSubtitlesPresent { get; set; } public bool SkipSubtitlesIfAudioTrackMatches { get; set; } diff --git a/MediaBrowser.Model/Configuration/MetadataPluginType.cs b/MediaBrowser.Model/Configuration/MetadataPluginType.cs index ef303726d1..670d6e3837 100644 --- a/MediaBrowser.Model/Configuration/MetadataPluginType.cs +++ b/MediaBrowser.Model/Configuration/MetadataPluginType.cs @@ -14,6 +14,7 @@ namespace MediaBrowser.Model.Configuration MetadataFetcher, MetadataSaver, SubtitleFetcher, - LyricFetcher + LyricFetcher, + MediaSegmentProvider } } diff --git a/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs b/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs new file mode 100644 index 0000000000..8c1f44de8c --- /dev/null +++ b/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs @@ -0,0 +1,14 @@ +using System; + +namespace MediaBrowser.Model; + +/// +/// Model containing the arguments for enumerating the requested media item. +/// +public record MediaSegmentGenerationRequest +{ + /// + /// Gets the Id to the BaseItem the segments should be extracted from. + /// + public Guid ItemId { get; init; } +} diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 60d89a51b7..81a9af68be 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -62,7 +62,7 @@ namespace MediaBrowser.Providers.Manager private readonly CancellationTokenSource _disposeCancellationTokenSource = new(); private readonly PriorityQueue<(Guid ItemId, MetadataRefreshOptions RefreshOptions), RefreshPriority> _refreshQueue = new(); private readonly IMemoryCache _memoryCache; - + private readonly IMediaSegmentManager _mediaSegmentManager; private readonly AsyncKeyedLocker _imageSaveLock = new(o => { o.PoolSize = 20; @@ -92,6 +92,7 @@ namespace MediaBrowser.Providers.Manager /// The BaseItem manager. /// The lyric manager. /// The memory cache. + /// The media segment manager. public ProviderManager( IHttpClientFactory httpClientFactory, ISubtitleManager subtitleManager, @@ -103,7 +104,8 @@ namespace MediaBrowser.Providers.Manager ILibraryManager libraryManager, IBaseItemManager baseItemManager, ILyricManager lyricManager, - IMemoryCache memoryCache) + IMemoryCache memoryCache, + IMediaSegmentManager mediaSegmentManager) { _logger = logger; _httpClientFactory = httpClientFactory; @@ -116,6 +118,7 @@ namespace MediaBrowser.Providers.Manager _baseItemManager = baseItemManager; _lyricManager = lyricManager; _memoryCache = memoryCache; + _mediaSegmentManager = mediaSegmentManager; } /// @@ -572,6 +575,14 @@ namespace MediaBrowser.Providers.Manager Type = MetadataPluginType.LyricFetcher })); + // Media segment providers + var mediaSegmentProviders = _mediaSegmentManager.GetSupportedProviders(dummy); + pluginList.AddRange(mediaSegmentProviders.Select(i => new MetadataPlugin + { + Name = i.Name, + Type = MetadataPluginType.MediaSegmentProvider + })); + summary.Plugins = pluginList.ToArray(); var supportedImageTypes = imageProviders.OfType() diff --git a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs index cced2b1e26..c227883b50 100644 --- a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs +++ b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs @@ -574,7 +574,8 @@ namespace Jellyfin.Providers.Tests.Manager libraryManager.Object, baseItemManager!, Mock.Of(), - Mock.Of()); + Mock.Of(), + Mock.Of()); return providerManager; } -- cgit v1.2.3 From 2351eeba561905bafae48a948f3126797c284766 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 17 Sep 2024 20:29:43 +0200 Subject: Rework PR 6203 --- .../Devices/DeviceManager.cs | 4 +- MediaBrowser.Model/Dlna/CodecProfile.cs | 136 +- MediaBrowser.Model/Dlna/ContainerProfile.cs | 107 +- MediaBrowser.Model/Dlna/DeviceProfile.cs | 109 +- MediaBrowser.Model/Dlna/DirectPlayProfile.cs | 79 +- MediaBrowser.Model/Dlna/StreamBuilder.cs | 192 ++- MediaBrowser.Model/Dlna/StreamInfo.cs | 1675 ++++++++++++-------- MediaBrowser.Model/Dlna/SubtitleProfile.cs | 84 +- MediaBrowser.Model/Dlna/TranscodingProfile.cs | 196 ++- MediaBrowser.Model/Extensions/ContainerHelper.cs | 145 ++ .../MediaInfo/AudioFileProber.cs | 2 +- .../Dlna/ContainerHelperTests.cs | 54 + .../Dlna/ContainerProfileTests.cs | 19 - .../Dlna/StreamBuilderTests.cs | 21 +- 14 files changed, 1690 insertions(+), 1133 deletions(-) create mode 100644 MediaBrowser.Model/Extensions/ContainerHelper.cs create mode 100644 tests/Jellyfin.Model.Tests/Dlna/ContainerHelperTests.cs delete mode 100644 tests/Jellyfin.Model.Tests/Dlna/ContainerProfileTests.cs (limited to 'Jellyfin.Server.Implementations') diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index d7a46e2d54..415c04bbf1 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -135,8 +135,8 @@ namespace Jellyfin.Server.Implementations.Devices { IEnumerable devices = _devices.Values .Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value)) - .Where(device => query.DeviceId == null || device.DeviceId == query.DeviceId) - .Where(device => query.AccessToken == null || device.AccessToken == query.AccessToken) + .Where(device => query.DeviceId is null || device.DeviceId == query.DeviceId) + .Where(device => query.AccessToken is null || device.AccessToken == query.AccessToken) .OrderBy(d => d.Id) .ToList(); var count = devices.Count(); diff --git a/MediaBrowser.Model/Dlna/CodecProfile.cs b/MediaBrowser.Model/Dlna/CodecProfile.cs index 07c1a29a4f..da34eddcd1 100644 --- a/MediaBrowser.Model/Dlna/CodecProfile.cs +++ b/MediaBrowser.Model/Dlna/CodecProfile.cs @@ -1,74 +1,94 @@ -#nullable disable -#pragma warning disable CS1591 - using System; +using System.Collections.Generic; +using System.Linq; using System.Xml.Serialization; -using Jellyfin.Extensions; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Model.Dlna; -namespace MediaBrowser.Model.Dlna +/// +/// Defines the . +/// +public class CodecProfile { - public class CodecProfile + /// + /// Initializes a new instance of the class. + /// + public CodecProfile() { - public CodecProfile() - { - Conditions = Array.Empty(); - ApplyConditions = Array.Empty(); - } - - [XmlAttribute("type")] - public CodecType Type { get; set; } - - public ProfileCondition[] Conditions { get; set; } - - public ProfileCondition[] ApplyConditions { get; set; } - - [XmlAttribute("codec")] - public string Codec { get; set; } + Conditions = []; + ApplyConditions = []; + } - [XmlAttribute("container")] - public string Container { get; set; } + /// + /// Gets or sets the which this container must meet. + /// + [XmlAttribute("type")] + public CodecType Type { get; set; } - [XmlAttribute("subcontainer")] - public string SubContainer { get; set; } + /// + /// Gets or sets the list of which this profile must meet. + /// + public ProfileCondition[] Conditions { get; set; } - public string[] GetCodecs() - { - return ContainerProfile.SplitValue(Codec); - } + /// + /// Gets or sets the list of to apply if this profile is met. + /// + public ProfileCondition[] ApplyConditions { get; set; } - private bool ContainsContainer(string container, bool useSubContainer = false) - { - var containerToCheck = useSubContainer && string.Equals(Container, "hls", StringComparison.OrdinalIgnoreCase) ? SubContainer : Container; - return ContainerProfile.ContainsContainer(containerToCheck, container); - } + /// + /// Gets or sets the codec(s) that this profile applies to. + /// + [XmlAttribute("codec")] + public string? Codec { get; set; } - public bool ContainsAnyCodec(string codec, string container, bool useSubContainer = false) - { - return ContainsAnyCodec(ContainerProfile.SplitValue(codec), container, useSubContainer); - } + /// + /// Gets or sets the container(s) which this profile will be applied to. + /// + [XmlAttribute("container")] + public string? Container { get; set; } - public bool ContainsAnyCodec(string[] codec, string container, bool useSubContainer = false) - { - if (!ContainsContainer(container, useSubContainer)) - { - return false; - } + /// + /// Gets or sets the sub-container(s) which this profile will be applied to. + /// + [XmlAttribute("subcontainer")] + public string? SubContainer { get; set; } - var codecs = GetCodecs(); - if (codecs.Length == 0) - { - return true; - } + /// + /// Checks to see whether the codecs and containers contain the given parameters. + /// + /// The codecs to match. + /// The container to match. + /// Consider sub-containers. + /// True if both conditions are met. + public bool ContainsAnyCodec(IReadOnlyList codecs, string? container, bool useSubContainer = false) + { + var containerToCheck = useSubContainer && string.Equals(Container, "hls", StringComparison.OrdinalIgnoreCase) ? SubContainer : Container; + return ContainerHelper.ContainsContainer(containerToCheck, container) && codecs.Any(c => ContainerHelper.ContainsContainer(Codec, false, c)); + } - foreach (var val in codec) - { - if (codecs.Contains(val, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } + /// + /// Checks to see whether the codecs and containers contain the given parameters. + /// + /// The codec to match. + /// The container to match. + /// Consider sub-containers. + /// True if both conditions are met. + public bool ContainsAnyCodec(string? codec, string? container, bool useSubContainer = false) + { + return ContainsAnyCodec(codec.AsSpan(), container, useSubContainer); + } - return false; - } + /// + /// Checks to see whether the codecs and containers contain the given parameters. + /// + /// The codec to match. + /// The container to match. + /// Consider sub-containers. + /// True if both conditions are met. + public bool ContainsAnyCodec(ReadOnlySpan codec, string? container, bool useSubContainer = false) + { + var containerToCheck = useSubContainer && string.Equals(Container, "hls", StringComparison.OrdinalIgnoreCase) ? SubContainer : Container; + return ContainerHelper.ContainsContainer(containerToCheck, container) && ContainerHelper.ContainsContainer(Codec, false, codec); } } diff --git a/MediaBrowser.Model/Dlna/ContainerProfile.cs b/MediaBrowser.Model/Dlna/ContainerProfile.cs index 9780042684..a421799075 100644 --- a/MediaBrowser.Model/Dlna/ContainerProfile.cs +++ b/MediaBrowser.Model/Dlna/ContainerProfile.cs @@ -1,74 +1,49 @@ -#pragma warning disable CS1591 +#pragma warning disable CA1819 // Properties should not return arrays using System; +using System.Collections.Generic; using System.Xml.Serialization; -using Jellyfin.Extensions; +using MediaBrowser.Model.Extensions; -namespace MediaBrowser.Model.Dlna +namespace MediaBrowser.Model.Dlna; + +/// +/// Defines the . +/// +public class ContainerProfile { - public class ContainerProfile + /// + /// Gets or sets the which this container must meet. + /// + [XmlAttribute("type")] + public DlnaProfileType Type { get; set; } + + /// + /// Gets or sets the list of which this container will be applied to. + /// + public ProfileCondition[] Conditions { get; set; } = []; + + /// + /// Gets or sets the container(s) which this container must meet. + /// + [XmlAttribute("container")] + public string? Container { get; set; } + + /// + /// Gets or sets the sub container(s) which this container must meet. + /// + [XmlAttribute("subcontainer")] + public string? SubContainer { get; set; } + + /// + /// Returns true if an item in appears in the property. + /// + /// The item to match. + /// Consider subcontainers. + /// The result of the operation. + public bool ContainsContainer(ReadOnlySpan container, bool useSubContainer = false) { - [XmlAttribute("type")] - public DlnaProfileType Type { get; set; } - - public ProfileCondition[] Conditions { get; set; } = Array.Empty(); - - [XmlAttribute("container")] - public string Container { get; set; } = string.Empty; - - public static string[] SplitValue(string? value) - { - if (string.IsNullOrEmpty(value)) - { - return Array.Empty(); - } - - return value.Split(',', StringSplitOptions.RemoveEmptyEntries); - } - - public bool ContainsContainer(string? container) - { - var containers = SplitValue(Container); - - return ContainsContainer(containers, container); - } - - public static bool ContainsContainer(string? profileContainers, string? inputContainer) - { - var isNegativeList = false; - if (profileContainers is not null && profileContainers.StartsWith('-')) - { - isNegativeList = true; - profileContainers = profileContainers.Substring(1); - } - - return ContainsContainer(SplitValue(profileContainers), isNegativeList, inputContainer); - } - - public static bool ContainsContainer(string[]? profileContainers, string? inputContainer) - { - return ContainsContainer(profileContainers, false, inputContainer); - } - - public static bool ContainsContainer(string[]? profileContainers, bool isNegativeList, string? inputContainer) - { - if (profileContainers is null || profileContainers.Length == 0) - { - // Empty profiles always support all containers/codecs - return true; - } - - var allInputContainers = SplitValue(inputContainer); - - foreach (var container in allInputContainers) - { - if (profileContainers.Contains(container, StringComparison.OrdinalIgnoreCase)) - { - return !isNegativeList; - } - } - - return isNegativeList; - } + var containerToCheck = useSubContainer && string.Equals(Container, "hls", StringComparison.OrdinalIgnoreCase) ? SubContainer : Container; + return ContainerHelper.ContainsContainer(containerToCheck, container); } } diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs index 2addebbfca..f689576222 100644 --- a/MediaBrowser.Model/Dlna/DeviceProfile.cs +++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs @@ -1,74 +1,71 @@ #pragma warning disable CA1819 // Properties should not return arrays using System; -using System.Xml.Serialization; -namespace MediaBrowser.Model.Dlna +namespace MediaBrowser.Model.Dlna; + +/// +/// A represents a set of metadata which determines which content a certain device is able to play. +///
+/// Specifically, it defines the supported containers and +/// codecs (video and/or audio, including codec profiles and levels) +/// the device is able to direct play (without transcoding or remuxing), +/// as well as which containers/codecs to transcode to in case it isn't. +///
+public class DeviceProfile { /// - /// A represents a set of metadata which determines which content a certain device is able to play. - ///
- /// Specifically, it defines the supported containers and - /// codecs (video and/or audio, including codec profiles and levels) - /// the device is able to direct play (without transcoding or remuxing), - /// as well as which containers/codecs to transcode to in case it isn't. + /// Gets or sets the name of this device profile. User profiles must have a unique name. ///
- public class DeviceProfile - { - /// - /// Gets or sets the name of this device profile. - /// - public string? Name { get; set; } + public string? Name { get; set; } - /// - /// Gets or sets the Id. - /// - [XmlIgnore] - public string? Id { get; set; } + /// + /// Gets or sets the unique internal identifier. + /// + public Guid Id { get; set; } - /// - /// Gets or sets the maximum allowed bitrate for all streamed content. - /// - public int? MaxStreamingBitrate { get; set; } = 8000000; + /// + /// Gets or sets the maximum allowed bitrate for all streamed content. + /// + public int? MaxStreamingBitrate { get; set; } = 8000000; - /// - /// Gets or sets the maximum allowed bitrate for statically streamed content (= direct played files). - /// - public int? MaxStaticBitrate { get; set; } = 8000000; + /// + /// Gets or sets the maximum allowed bitrate for statically streamed content (= direct played files). + /// + public int? MaxStaticBitrate { get; set; } = 8000000; - /// - /// Gets or sets the maximum allowed bitrate for transcoded music streams. - /// - public int? MusicStreamingTranscodingBitrate { get; set; } = 128000; + /// + /// Gets or sets the maximum allowed bitrate for transcoded music streams. + /// + public int? MusicStreamingTranscodingBitrate { get; set; } = 128000; - /// - /// Gets or sets the maximum allowed bitrate for statically streamed (= direct played) music files. - /// - public int? MaxStaticMusicBitrate { get; set; } = 8000000; + /// + /// Gets or sets the maximum allowed bitrate for statically streamed (= direct played) music files. + /// + public int? MaxStaticMusicBitrate { get; set; } = 8000000; - /// - /// Gets or sets the direct play profiles. - /// - public DirectPlayProfile[] DirectPlayProfiles { get; set; } = Array.Empty(); + /// + /// Gets or sets the direct play profiles. + /// + public DirectPlayProfile[] DirectPlayProfiles { get; set; } = []; - /// - /// Gets or sets the transcoding profiles. - /// - public TranscodingProfile[] TranscodingProfiles { get; set; } = Array.Empty(); + /// + /// Gets or sets the transcoding profiles. + /// + public TranscodingProfile[] TranscodingProfiles { get; set; } = []; - /// - /// Gets or sets the container profiles. - /// - public ContainerProfile[] ContainerProfiles { get; set; } = Array.Empty(); + /// + /// Gets or sets the container profiles. Failing to meet these optional conditions causes transcoding to occur. + /// + public ContainerProfile[] ContainerProfiles { get; set; } = []; - /// - /// Gets or sets the codec profiles. - /// - public CodecProfile[] CodecProfiles { get; set; } = Array.Empty(); + /// + /// Gets or sets the codec profiles. + /// + public CodecProfile[] CodecProfiles { get; set; } = []; - /// - /// Gets or sets the subtitle profiles. - /// - public SubtitleProfile[] SubtitleProfiles { get; set; } = Array.Empty(); - } + /// + /// Gets or sets the subtitle profiles. + /// + public SubtitleProfile[] SubtitleProfiles { get; set; } = []; } diff --git a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs index f68235d869..438df34415 100644 --- a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs +++ b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs @@ -1,36 +1,65 @@ -#pragma warning disable CS1591 - using System.Xml.Serialization; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Model.Dlna; -namespace MediaBrowser.Model.Dlna +/// +/// Defines the . +/// +public class DirectPlayProfile { - public class DirectPlayProfile - { - [XmlAttribute("container")] - public string? Container { get; set; } + /// + /// Gets or sets the container. + /// + [XmlAttribute("container")] + public string Container { get; set; } = string.Empty; - [XmlAttribute("audioCodec")] - public string? AudioCodec { get; set; } + /// + /// Gets or sets the audio codec. + /// + [XmlAttribute("audioCodec")] + public string? AudioCodec { get; set; } - [XmlAttribute("videoCodec")] - public string? VideoCodec { get; set; } + /// + /// Gets or sets the video codec. + /// + [XmlAttribute("videoCodec")] + public string? VideoCodec { get; set; } - [XmlAttribute("type")] - public DlnaProfileType Type { get; set; } + /// + /// Gets or sets the Dlna profile type. + /// + [XmlAttribute("type")] + public DlnaProfileType Type { get; set; } - public bool SupportsContainer(string? container) - { - return ContainerProfile.ContainsContainer(Container, container); - } + /// + /// Returns whether the supports the . + /// + /// The container to match against. + /// True if supported. + public bool SupportsContainer(string? container) + { + return ContainerHelper.ContainsContainer(Container, container); + } - public bool SupportsVideoCodec(string? codec) - { - return Type == DlnaProfileType.Video && ContainerProfile.ContainsContainer(VideoCodec, codec); - } + /// + /// Returns whether the supports the . + /// + /// The codec to match against. + /// True if supported. + public bool SupportsVideoCodec(string? codec) + { + return Type == DlnaProfileType.Video && ContainerHelper.ContainsContainer(VideoCodec, codec); + } - public bool SupportsAudioCodec(string? codec) - { - return (Type == DlnaProfileType.Audio || Type == DlnaProfileType.Video) && ContainerProfile.ContainsContainer(AudioCodec, codec); - } + /// + /// Returns whether the supports the . + /// + /// The codec to match against. + /// True if supported. + public bool SupportsAudioCodec(string? codec) + { + // Video profiles can have audio codec restrictions too, therefore incude Video as valid type. + return (Type == DlnaProfileType.Audio || Type == DlnaProfileType.Video) && ContainerHelper.ContainsContainer(AudioCodec, codec); } } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index bf612f0ac0..6fc7f796de 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -6,6 +6,7 @@ using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; @@ -27,9 +28,9 @@ namespace MediaBrowser.Model.Dlna private readonly ILogger _logger; private readonly ITranscoderSupport _transcoderSupport; - private static readonly string[] _supportedHlsVideoCodecs = new string[] { "h264", "hevc", "vp9", "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" }; + private static readonly string[] _supportedHlsVideoCodecs = ["h264", "hevc", "vp9", "av1"]; + private static readonly string[] _supportedHlsAudioCodecsTs = ["aac", "ac3", "eac3", "mp3"]; + private static readonly string[] _supportedHlsAudioCodecsMp4 = ["aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd"]; /// /// Initializes a new instance of the class. @@ -51,7 +52,7 @@ namespace MediaBrowser.Model.Dlna { ValidateMediaOptions(options, false); - var streams = new List(); + List streams = []; foreach (var mediaSource in options.MediaSources) { if (!(string.IsNullOrEmpty(options.MediaSourceId) @@ -64,7 +65,7 @@ namespace MediaBrowser.Model.Dlna if (streamInfo is not null) { streamInfo.DeviceId = options.DeviceId; - streamInfo.DeviceProfileId = options.Profile.Id; + streamInfo.DeviceProfileId = options.Profile.Id.ToString("N", CultureInfo.InvariantCulture); streams.Add(streamInfo); } } @@ -129,7 +130,7 @@ namespace MediaBrowser.Model.Dlna if (directPlayMethod is PlayMethod.DirectStream) { var remuxContainer = item.TranscodingContainer ?? "ts"; - var supportedHlsContainers = new[] { "ts", "mp4" }; + string[] supportedHlsContainers = ["ts", "mp4"]; // If the container specified for the profile is an HLS supported container, use that container instead, overriding the preference // The client should be responsible to ensure this container is compatible remuxContainer = Array.Exists(supportedHlsContainers, element => string.Equals(element, directPlayInfo.Profile?.Container, StringComparison.OrdinalIgnoreCase)) ? directPlayInfo.Profile?.Container : remuxContainer; @@ -226,7 +227,7 @@ namespace MediaBrowser.Model.Dlna ? options.MediaSources : options.MediaSources.Where(x => string.Equals(x.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)); - var streams = new List(); + List streams = []; foreach (var mediaSourceInfo in mediaSources) { var streamInfo = BuildVideoItem(mediaSourceInfo, options); @@ -239,7 +240,7 @@ namespace MediaBrowser.Model.Dlna foreach (var stream in streams) { stream.DeviceId = options.DeviceId; - stream.DeviceProfileId = options.Profile.Id; + stream.DeviceProfileId = options.Profile.Id.ToString("N", CultureInfo.InvariantCulture); } return GetOptimalStream(streams, options.GetMaxBitrate(false) ?? 0); @@ -388,32 +389,33 @@ namespace MediaBrowser.Model.Dlna /// The . /// The object to get the video stream from. /// The normalized input container. - public static string? NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile? profile, DlnaProfileType type, DirectPlayProfile? playProfile = null) + public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile? profile, DlnaProfileType type, DirectPlayProfile? playProfile = null) { - if (string.IsNullOrEmpty(inputContainer)) + if (profile is null || !inputContainer.Contains(',', StringComparison.OrdinalIgnoreCase)) { - return null; + return inputContainer; } - var formats = ContainerProfile.SplitValue(inputContainer); - - if (profile is not null) + var formats = ContainerHelper.Split(inputContainer); + var playProfiles = playProfile is null ? profile.DirectPlayProfiles : [playProfile]; + foreach (var format in formats) { - var playProfiles = playProfile is null ? profile.DirectPlayProfiles : new[] { playProfile }; - foreach (var format in formats) + foreach (var directPlayProfile in playProfiles) { - foreach (var directPlayProfile in playProfiles) + if (directPlayProfile.Type != type) { - if (directPlayProfile.Type == type - && directPlayProfile.SupportsContainer(format)) - { - return format; - } + continue; + } + + var formatStr = format.ToString(); + if (directPlayProfile.SupportsContainer(formatStr)) + { + return formatStr; } } } - return formats[0]; + return inputContainer; } private (DirectPlayProfile? Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, MediaOptions options) @@ -533,7 +535,6 @@ namespace MediaBrowser.Model.Dlna private static int? GetDefaultSubtitleStreamIndex(MediaSourceInfo item, SubtitleProfile[] subtitleProfiles) { int highestScore = -1; - foreach (var stream in item.MediaStreams) { if (stream.Type == MediaStreamType.Subtitle @@ -544,7 +545,7 @@ namespace MediaBrowser.Model.Dlna } } - var topStreams = new List(); + List topStreams = []; foreach (var stream in item.MediaStreams) { if (stream.Type == MediaStreamType.Subtitle && stream.Score.HasValue && stream.Score.Value == highestScore) @@ -623,8 +624,8 @@ namespace MediaBrowser.Model.Dlna playlistItem.Container = container; playlistItem.SubProtocol = protocol; - playlistItem.VideoCodecs = new[] { item.VideoStream.Codec }; - playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile?.AudioCodec); + playlistItem.VideoCodecs = [item.VideoStream.Codec]; + playlistItem.AudioCodecs = ContainerHelper.Split(directPlayProfile?.AudioCodec); } private StreamInfo BuildVideoItem(MediaSourceInfo item, MediaOptions options) @@ -651,7 +652,7 @@ namespace MediaBrowser.Model.Dlna } // Collect candidate audio streams - ICollection candidateAudioStreams = audioStream is null ? Array.Empty() : new[] { audioStream }; + ICollection candidateAudioStreams = audioStream is null ? [] : [audioStream]; if (!options.AudioStreamIndex.HasValue || options.AudioStreamIndex < 0) { if (audioStream?.IsDefault == true) @@ -702,7 +703,8 @@ namespace MediaBrowser.Model.Dlna directPlayProfile = directPlayInfo.Profile; playlistItem.PlayMethod = directPlay.Value; playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile); - playlistItem.VideoCodecs = new[] { videoStream.Codec }; + var videoCodec = videoStream?.Codec; + playlistItem.VideoCodecs = videoCodec is null ? [] : [videoCodec]; if (directPlay == PlayMethod.DirectPlay) { @@ -713,7 +715,7 @@ namespace MediaBrowser.Model.Dlna { playlistItem.AudioStreamIndex = audioStreamIndex; var audioCodec = item.GetMediaStream(MediaStreamType.Audio, audioStreamIndex.Value)?.Codec; - playlistItem.AudioCodecs = audioCodec is null ? Array.Empty() : new[] { audioCodec }; + playlistItem.AudioCodecs = audioCodec is null ? [] : [audioCodec]; } } else if (directPlay == PlayMethod.DirectStream) @@ -721,7 +723,7 @@ namespace MediaBrowser.Model.Dlna playlistItem.AudioStreamIndex = audioStream?.Index; if (audioStream is not null) { - playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile?.AudioCodec); + playlistItem.AudioCodecs = ContainerHelper.Split(directPlayProfile?.AudioCodec); } SetStreamInfoOptionsFromDirectPlayProfile(options, item, playlistItem, directPlayProfile); @@ -753,7 +755,7 @@ namespace MediaBrowser.Model.Dlna { // Can't direct play, find the transcoding profile // If we do this for direct-stream we will overwrite the info - var (transcodingProfile, playMethod) = GetVideoTranscodeProfile(item, options, videoStream, audioStream, candidateAudioStreams, subtitleStream, playlistItem); + var (transcodingProfile, playMethod) = GetVideoTranscodeProfile(item, options, videoStream, audioStream, playlistItem); if (transcodingProfile is not null && playMethod.HasValue) { @@ -781,7 +783,7 @@ namespace MediaBrowser.Model.Dlna } playlistItem.SubtitleFormat = subtitleProfile.Format; - playlistItem.SubtitleCodecs = new[] { subtitleProfile.Format }; + playlistItem.SubtitleCodecs = [subtitleProfile.Format]; } if ((playlistItem.TranscodeReasons & (VideoReasons | TranscodeReason.ContainerBitrateExceedsLimit)) != 0) @@ -810,8 +812,6 @@ namespace MediaBrowser.Model.Dlna MediaOptions options, MediaStream? videoStream, MediaStream? audioStream, - IEnumerable candidateAudioStreams, - MediaStream? subtitleStream, StreamInfo playlistItem) { if (!(item.SupportsTranscoding || item.SupportsDirectStream)) @@ -849,9 +849,7 @@ namespace MediaBrowser.Model.Dlna if (options.AllowVideoStreamCopy) { - var videoCodecs = ContainerProfile.SplitValue(transcodingProfile.VideoCodec); - - if (ContainerProfile.ContainsContainer(videoCodecs, videoCodec)) + if (ContainerHelper.ContainsContainer(transcodingProfile.VideoCodec, videoCodec)) { var appliedVideoConditions = options.Profile.CodecProfiles .Where(i => i.Type == CodecType.Video && @@ -868,9 +866,7 @@ namespace MediaBrowser.Model.Dlna if (options.AllowAudioStreamCopy) { - var audioCodecs = ContainerProfile.SplitValue(transcodingProfile.AudioCodec); - - if (ContainerProfile.ContainsContainer(audioCodecs, audioCodec)) + if (ContainerHelper.ContainsContainer(transcodingProfile.AudioCodec, audioCodec)) { var appliedVideoConditions = options.Profile.CodecProfiles .Where(i => i.Type == CodecType.VideoAudio && @@ -913,20 +909,18 @@ namespace MediaBrowser.Model.Dlna string? audioCodec) { // Prefer matching video codecs - var videoCodecs = ContainerProfile.SplitValue(videoCodec); + var videoCodecs = ContainerHelper.Split(videoCodec).ToList(); - // Enforce HLS video codec restrictions - if (playlistItem.SubProtocol == MediaStreamProtocol.hls) + if (videoCodecs.Count == 0 && videoStream is not null) { - videoCodecs = videoCodecs.Where(codec => _supportedHlsVideoCodecs.Contains(codec)).ToArray(); + // Add the original codec if no codec is specified + videoCodecs.Add(videoStream.Codec); } - var directVideoCodec = ContainerProfile.ContainsContainer(videoCodecs, videoStream?.Codec) ? videoStream?.Codec : null; - if (directVideoCodec is not null) + // Enforce HLS video codec restrictions + if (playlistItem.SubProtocol == MediaStreamProtocol.hls) { - // merge directVideoCodec to videoCodecs - Array.Resize(ref videoCodecs, videoCodecs.Length + 1); - videoCodecs[^1] = directVideoCodec; + videoCodecs = videoCodecs.Where(codec => _supportedHlsVideoCodecs.Contains(codec)).ToList(); } playlistItem.VideoCodecs = videoCodecs; @@ -950,22 +944,28 @@ namespace MediaBrowser.Model.Dlna } // Prefer matching audio codecs, could do better here - var audioCodecs = ContainerProfile.SplitValue(audioCodec); + var audioCodecs = ContainerHelper.Split(audioCodec).ToList(); + + if (audioCodecs.Count == 0 && audioStream is not null) + { + // Add the original codec if no codec is specified + audioCodecs.Add(audioStream.Codec); + } // Enforce HLS audio codec restrictions if (playlistItem.SubProtocol == MediaStreamProtocol.hls) { if (string.Equals(playlistItem.Container, "mp4", StringComparison.OrdinalIgnoreCase)) { - audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsMp4.Contains(codec)).ToArray(); + audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsMp4.Contains(codec)).ToList(); } else { - audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsTs.Contains(codec)).ToArray(); + audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsTs.Contains(codec)).ToList(); } } - var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec)).FirstOrDefault(); + var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerHelper.ContainsContainer(audioCodecs, false, stream.Codec)).FirstOrDefault(); var directAudioStream = audioStreamWithSupportedCodec?.Channels is not null && audioStreamWithSupportedCodec.Channels.Value <= (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue) ? audioStreamWithSupportedCodec : null; @@ -982,7 +982,8 @@ namespace MediaBrowser.Model.Dlna { audioStream = directAudioStream; playlistItem.AudioStreamIndex = audioStream.Index; - playlistItem.AudioCodecs = audioCodecs = new[] { audioStream.Codec }; + audioCodecs = [audioStream.Codec]; + playlistItem.AudioCodecs = audioCodecs; // Copy matching audio codec options playlistItem.AudioSampleRate = audioStream.SampleRate; @@ -1023,18 +1024,17 @@ namespace MediaBrowser.Model.Dlna var appliedVideoConditions = options.Profile.CodecProfiles .Where(i => i.Type == CodecType.Video && - i.ContainsAnyCodec(videoCodecs, container, useSubContainer) && + i.ContainsAnyCodec(playlistItem.VideoCodecs, container, useSubContainer) && i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))) // Reverse codec profiles for backward compatibility - first codec profile has higher priority .Reverse(); - - foreach (var i in appliedVideoConditions) + foreach (var condition in appliedVideoConditions) { - foreach (var transcodingVideoCodec in videoCodecs) + foreach (var transcodingVideoCodec in playlistItem.VideoCodecs) { - if (i.ContainsAnyCodec(transcodingVideoCodec, container, useSubContainer)) + if (condition.ContainsAnyCodec(transcodingVideoCodec, container, useSubContainer)) { - ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, true); + ApplyTranscodingConditions(playlistItem, condition.Conditions, transcodingVideoCodec, true, true); continue; } } @@ -1055,14 +1055,14 @@ namespace MediaBrowser.Model.Dlna var appliedAudioConditions = options.Profile.CodecProfiles .Where(i => i.Type == CodecType.VideoAudio && - i.ContainsAnyCodec(audioCodecs, container) && + i.ContainsAnyCodec(playlistItem.AudioCodecs, container) && i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio))) // Reverse codec profiles for backward compatibility - first codec profile has higher priority .Reverse(); foreach (var codecProfile in appliedAudioConditions) { - foreach (var transcodingAudioCodec in audioCodecs) + foreach (var transcodingAudioCodec in playlistItem.AudioCodecs) { if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container)) { @@ -1132,9 +1132,9 @@ namespace MediaBrowser.Model.Dlna return 192000; } - private static int GetAudioBitrate(long maxTotalBitrate, string[] targetAudioCodecs, MediaStream? audioStream, StreamInfo item) + private static int GetAudioBitrate(long maxTotalBitrate, IReadOnlyList targetAudioCodecs, MediaStream? audioStream, StreamInfo item) { - string? targetAudioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0]; + string? targetAudioCodec = targetAudioCodecs.Count == 0 ? null : targetAudioCodecs[0]; int? targetAudioChannels = item.GetTargetAudioChannels(targetAudioCodec); @@ -1151,7 +1151,7 @@ namespace MediaBrowser.Model.Dlna && audioStream.Channels.HasValue && audioStream.Channels.Value > targetAudioChannels.Value) { - // Reduce the bitrate if we're downmixing. + // Reduce the bitrate if we're down mixing. defaultBitrate = GetDefaultAudioBitrate(targetAudioCodec, targetAudioChannels); } else if (targetAudioChannels.HasValue @@ -1159,8 +1159,8 @@ namespace MediaBrowser.Model.Dlna && audioStream.Channels.Value <= targetAudioChannels.Value && !string.IsNullOrEmpty(audioStream.Codec) && targetAudioCodecs is not null - && targetAudioCodecs.Length > 0 - && !Array.Exists(targetAudioCodecs, elem => string.Equals(audioStream.Codec, elem, StringComparison.OrdinalIgnoreCase))) + && targetAudioCodecs.Count > 0 + && !targetAudioCodecs.Any(elem => string.Equals(audioStream.Codec, elem, StringComparison.OrdinalIgnoreCase))) { // Shift the bitrate if we're transcoding to a different audio codec. defaultBitrate = GetDefaultAudioBitrate(targetAudioCodec, audioStream.Channels.Value); @@ -1299,7 +1299,7 @@ namespace MediaBrowser.Model.Dlna !checkVideoConditions(codecProfile.ApplyConditions).Any()) .SelectMany(codecProfile => checkVideoConditions(codecProfile.Conditions))); - // Check audiocandidates profile conditions + // Check audio candidates profile conditions var audioStreamMatches = candidateAudioStreams.ToDictionary(s => s, audioStream => CheckVideoAudioStreamDirectPlay(options, mediaSource, container, audioStream)); TranscodeReason subtitleProfileReasons = 0; @@ -1316,24 +1316,6 @@ namespace MediaBrowser.Model.Dlna } } - var rankings = new[] { TranscodeReason.VideoCodecNotSupported, VideoCodecReasons, TranscodeReason.AudioCodecNotSupported, AudioCodecReasons, ContainerReasons }; - var rank = (ref TranscodeReason a) => - { - var index = 1; - foreach (var flag in rankings) - { - var reason = a & flag; - if (reason != 0) - { - return index; - } - - index++; - } - - return index; - }; - var containerSupported = false; // Check DirectPlay profiles to see if it can be direct played @@ -1400,7 +1382,9 @@ namespace MediaBrowser.Model.Dlna playMethod = PlayMethod.DirectStream; } - var ranked = rank(ref failureReasons); + TranscodeReason[] rankings = [TranscodeReason.VideoCodecNotSupported, VideoCodecReasons, TranscodeReason.AudioCodecNotSupported, AudioCodecReasons, ContainerReasons]; + var ranked = GetRank(ref failureReasons, rankings); + return (Result: (Profile: directPlayProfile, PlayMethod: playMethod, AudioStreamIndex: selectedAudioStream?.Index, TranscodeReason: failureReasons), Order: order, Rank: ranked); }) .OrderByDescending(analysis => analysis.Result.PlayMethod) @@ -1475,7 +1459,7 @@ namespace MediaBrowser.Model.Dlna /// The . /// The . /// The output container. - /// The subtitle transoding protocol. + /// The subtitle transcoding protocol. /// The normalized input container. public static SubtitleProfile GetSubtitleProfile( MediaSourceInfo mediaSource, @@ -1501,7 +1485,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (!ContainerProfile.ContainsContainer(profile.Container, outputContainer)) + if (!ContainerHelper.ContainsContainer(profile.Container, outputContainer)) { continue; } @@ -1530,7 +1514,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (!ContainerProfile.ContainsContainer(profile.Container, outputContainer)) + if (!ContainerHelper.ContainsContainer(profile.Container, outputContainer)) { continue; } @@ -1561,17 +1545,12 @@ namespace MediaBrowser.Model.Dlna { if (!string.IsNullOrEmpty(transcodingContainer)) { - string[] normalizedContainers = ContainerProfile.SplitValue(transcodingContainer); - - if (ContainerProfile.ContainsContainer(normalizedContainers, "ts") - || ContainerProfile.ContainsContainer(normalizedContainers, "mpegts") - || ContainerProfile.ContainsContainer(normalizedContainers, "mp4")) + if (ContainerHelper.ContainsContainer(transcodingContainer, "ts,mpegts,mp4")) { return false; } - if (ContainerProfile.ContainsContainer(normalizedContainers, "mkv") - || ContainerProfile.ContainsContainer(normalizedContainers, "matroska")) + if (ContainerHelper.ContainsContainer(transcodingContainer, "mkv,matroska")) { return true; } @@ -2274,5 +2253,22 @@ namespace MediaBrowser.Model.Dlna return false; } + + private int GetRank(ref TranscodeReason a, TranscodeReason[] rankings) + { + var index = 1; + foreach (var flag in rankings) + { + var reason = a & flag; + if (reason != 0) + { + return index; + } + + index++; + } + + return index; + } } } diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 8232ee3fe5..3be6860880 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -1,9 +1,6 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; using Jellyfin.Data.Enums; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; @@ -11,1007 +8,1303 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Session; -namespace MediaBrowser.Model.Dlna +namespace MediaBrowser.Model.Dlna; + +/// +/// Class holding information on a stream. +/// +public class StreamInfo { /// - /// Class StreamInfo. + /// Initializes a new instance of the class. /// - public class StreamInfo + public StreamInfo() { - public StreamInfo() - { - AudioCodecs = Array.Empty(); - VideoCodecs = Array.Empty(); - SubtitleCodecs = Array.Empty(); - StreamOptions = new Dictionary(StringComparer.OrdinalIgnoreCase); - } + AudioCodecs = []; + VideoCodecs = []; + SubtitleCodecs = []; + StreamOptions = new Dictionary(StringComparer.OrdinalIgnoreCase); + } - public Guid ItemId { get; set; } + /// + /// Gets or sets the item id. + /// + /// The item id. + public Guid ItemId { get; set; } - public PlayMethod PlayMethod { get; set; } + /// + /// Gets or sets the play method. + /// + /// The play method. + public PlayMethod PlayMethod { get; set; } - public EncodingContext Context { get; set; } + /// + /// Gets or sets the encoding context. + /// + /// The encoding context. + public EncodingContext Context { get; set; } - public DlnaProfileType MediaType { get; set; } + /// + /// Gets or sets the media type. + /// + /// The media type. + public DlnaProfileType MediaType { get; set; } - public string? Container { get; set; } + /// + /// Gets or sets the container. + /// + /// The container. + public string? Container { get; set; } - public MediaStreamProtocol SubProtocol { get; set; } + /// + /// Gets or sets the sub protocol. + /// + /// The sub protocol. + public MediaStreamProtocol SubProtocol { get; set; } - public long StartPositionTicks { get; set; } + /// + /// Gets or sets the start position ticks. + /// + /// The start position ticks. + public long StartPositionTicks { get; set; } - public int? SegmentLength { get; set; } + /// + /// Gets or sets the segment length. + /// + /// The segment length. + public int? SegmentLength { get; set; } - public int? MinSegments { get; set; } + /// + /// Gets or sets the minimum segments count. + /// + /// The minimum segments count. + public int? MinSegments { get; set; } - public bool BreakOnNonKeyFrames { get; set; } + /// + /// Gets or sets a value indicating whether the stream can be broken on non-keyframes. + /// + public bool BreakOnNonKeyFrames { get; set; } - public bool RequireAvc { get; set; } + /// + /// Gets or sets a value indicating whether the stream requires AVC. + /// + public bool RequireAvc { get; set; } - public bool RequireNonAnamorphic { get; set; } + /// + /// Gets or sets a value indicating whether the stream requires AVC. + /// + public bool RequireNonAnamorphic { get; set; } - public bool CopyTimestamps { get; set; } + /// + /// Gets or sets a value indicating whether timestamps should be copied. + /// + public bool CopyTimestamps { get; set; } - public bool EnableMpegtsM2TsMode { get; set; } + /// + /// Gets or sets a value indicating whether timestamps should be copied. + /// + public bool EnableMpegtsM2TsMode { get; set; } - public bool EnableSubtitlesInManifest { get; set; } + /// + /// Gets or sets a value indicating whether the subtitle manifest is enabled. + /// + public bool EnableSubtitlesInManifest { get; set; } - public string[] AudioCodecs { get; set; } + /// + /// Gets or sets the audio codecs. + /// + /// The audio codecs. + public IReadOnlyList AudioCodecs { get; set; } - public string[] VideoCodecs { get; set; } + /// + /// Gets or sets the video codecs. + /// + /// The video codecs. + public IReadOnlyList VideoCodecs { get; set; } - public int? AudioStreamIndex { get; set; } + /// + /// Gets or sets the audio stream index. + /// + /// The audio stream index. + public int? AudioStreamIndex { get; set; } - public int? SubtitleStreamIndex { get; set; } + /// + /// Gets or sets the video stream index. + /// + /// The subtitle stream index. + public int? SubtitleStreamIndex { get; set; } - public int? TranscodingMaxAudioChannels { get; set; } + /// + /// Gets or sets the maximum transcoding audio channels. + /// + /// The maximum transcoding audio channels. + public int? TranscodingMaxAudioChannels { get; set; } - public int? GlobalMaxAudioChannels { get; set; } + /// + /// Gets or sets the global maximum audio channels. + /// + /// The global maximum audio channels. + public int? GlobalMaxAudioChannels { get; set; } - public int? AudioBitrate { get; set; } + /// + /// Gets or sets the audio bitrate. + /// + /// The audio bitrate. + public int? AudioBitrate { get; set; } - public int? AudioSampleRate { get; set; } + /// + /// Gets or sets the audio sample rate. + /// + /// The audio sample rate. + public int? AudioSampleRate { get; set; } - public int? VideoBitrate { get; set; } + /// + /// Gets or sets the video bitrate. + /// + /// The video bitrate. + public int? VideoBitrate { get; set; } - public int? MaxWidth { get; set; } + /// + /// Gets or sets the maximum output width. + /// + /// The output width. + public int? MaxWidth { get; set; } - public int? MaxHeight { get; set; } + /// + /// Gets or sets the maximum output height. + /// + /// The maximum output height. + public int? MaxHeight { get; set; } - public float? MaxFramerate { get; set; } + /// + /// Gets or sets the maximum framerate. + /// + /// The maximum framerate. + public float? MaxFramerate { get; set; } - public required DeviceProfile DeviceProfile { get; set; } + /// + /// Gets or sets the device profile. + /// + /// The device profile. + public required DeviceProfile DeviceProfile { get; set; } - public string? DeviceProfileId { get; set; } + /// + /// Gets or sets the device profile id. + /// + /// The device profile id. + public string? DeviceProfileId { get; set; } - public string? DeviceId { get; set; } + /// + /// Gets or sets the device id. + /// + /// The device id. + public string? DeviceId { get; set; } - public long? RunTimeTicks { get; set; } + /// + /// Gets or sets the runtime ticks. + /// + /// The runtime ticks. + public long? RunTimeTicks { get; set; } - public TranscodeSeekInfo TranscodeSeekInfo { get; set; } + /// + /// Gets or sets the transcode seek info. + /// + /// The transcode seek info. + public TranscodeSeekInfo TranscodeSeekInfo { get; set; } - public bool EstimateContentLength { get; set; } + /// + /// Gets or sets a value indicating whether content length should be estimated. + /// + public bool EstimateContentLength { get; set; } - public MediaSourceInfo? MediaSource { get; set; } + /// + /// Gets or sets the media source info. + /// + /// The media source info. + public MediaSourceInfo? MediaSource { get; set; } - public string[] SubtitleCodecs { get; set; } + /// + /// Gets or sets the subtitle codecs. + /// + /// The subtitle codecs. + public IReadOnlyList SubtitleCodecs { get; set; } - public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; } + /// + /// Gets or sets the subtitle delivery method. + /// + /// The subtitle delivery method. + public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; } - public string? SubtitleFormat { get; set; } + /// + /// Gets or sets the subtitle format. + /// + /// The subtitle format. + public string? SubtitleFormat { get; set; } - public string? PlaySessionId { get; set; } + /// + /// Gets or sets the play session id. + /// + /// The play session id. + public string? PlaySessionId { get; set; } - public TranscodeReason TranscodeReasons { get; set; } + /// + /// Gets or sets the transcode reasons. + /// + /// The transcode reasons. + public TranscodeReason TranscodeReasons { get; set; } - public Dictionary StreamOptions { get; private set; } + /// + /// Gets the stream options. + /// + /// The stream options. + public Dictionary StreamOptions { get; private set; } - public string? MediaSourceId => MediaSource?.Id; + /// + /// Gets the media source id. + /// + /// The media source id. + public string? MediaSourceId => MediaSource?.Id; - public bool EnableAudioVbrEncoding { get; set; } + /// + /// Gets or sets a value indicating whether audio VBR encoding is enabled. + /// + public bool EnableAudioVbrEncoding { get; set; } - public bool IsDirectStream => MediaSource?.VideoType is not (VideoType.Dvd or VideoType.BluRay) - && PlayMethod is PlayMethod.DirectStream or PlayMethod.DirectPlay; + /// + /// Gets a value indicating whether the stream is direct. + /// + public bool IsDirectStream => MediaSource?.VideoType is not (VideoType.Dvd or VideoType.BluRay) + && PlayMethod is PlayMethod.DirectStream or PlayMethod.DirectPlay; - /// - /// Gets the audio stream that will be used. - /// - public MediaStream? TargetAudioStream => MediaSource?.GetDefaultAudioStream(AudioStreamIndex); + /// + /// Gets the audio stream that will be used in the output stream. + /// + /// The audio stream. + public MediaStream? TargetAudioStream => MediaSource?.GetDefaultAudioStream(AudioStreamIndex); - /// - /// Gets the video stream that will be used. - /// - public MediaStream? TargetVideoStream => MediaSource?.VideoStream; + /// + /// Gets the video stream that will be used in the output stream. + /// + /// The video stream. + public MediaStream? TargetVideoStream => MediaSource?.VideoStream; - /// - /// Gets the audio sample rate that will be in the output stream. - /// - public int? TargetAudioSampleRate + /// + /// Gets the audio sample rate that will be in the output stream. + /// + /// The target audio sample rate. + public int? TargetAudioSampleRate + { + get { - get - { - var stream = TargetAudioStream; - return AudioSampleRate.HasValue && !IsDirectStream - ? AudioSampleRate - : stream?.SampleRate; - } + var stream = TargetAudioStream; + return AudioSampleRate.HasValue && !IsDirectStream + ? AudioSampleRate + : stream?.SampleRate; } + } - /// - /// Gets the audio sample rate that will be in the output stream. - /// - public int? TargetAudioBitDepth + /// + /// Gets the audio bit depth that will be in the output stream. + /// + /// The target bit depth. + public int? TargetAudioBitDepth + { + get { - get + if (IsDirectStream) { - if (IsDirectStream) - { - return TargetAudioStream?.BitDepth; - } - - var targetAudioCodecs = TargetAudioCodec; - var audioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0]; - if (!string.IsNullOrEmpty(audioCodec)) - { - return GetTargetAudioBitDepth(audioCodec); - } - return TargetAudioStream?.BitDepth; } - } - /// - /// Gets the audio sample rate that will be in the output stream. - /// - public int? TargetVideoBitDepth - { - get + var targetAudioCodecs = TargetAudioCodec; + var audioCodec = targetAudioCodecs.Count == 0 ? null : targetAudioCodecs[0]; + if (!string.IsNullOrEmpty(audioCodec)) { - if (IsDirectStream) - { - return TargetVideoStream?.BitDepth; - } - - var targetVideoCodecs = TargetVideoCodec; - var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec)) - { - return GetTargetVideoBitDepth(videoCodec); - } - - return TargetVideoStream?.BitDepth; + return GetTargetAudioBitDepth(audioCodec); } + + return TargetAudioStream?.BitDepth; } + } - /// - /// Gets the target reference frames. - /// - /// The target reference frames. - public int? TargetRefFrames + /// + /// Gets the video bit depth that will be in the output stream. + /// + /// The target video bit depth. + public int? TargetVideoBitDepth + { + get { - get + if (IsDirectStream) { - if (IsDirectStream) - { - return TargetVideoStream?.RefFrames; - } - - var targetVideoCodecs = TargetVideoCodec; - var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec)) - { - return GetTargetRefFrames(videoCodec); - } + return TargetVideoStream?.BitDepth; + } - return TargetVideoStream?.RefFrames; + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) + { + return GetTargetVideoBitDepth(videoCodec); } + + return TargetVideoStream?.BitDepth; } + } - /// - /// Gets the audio sample rate that will be in the output stream. - /// - public float? TargetFramerate + /// + /// Gets the target reference frames that will be in the output stream. + /// + /// The target reference frames. + public int? TargetRefFrames + { + get { - get + if (IsDirectStream) { - var stream = TargetVideoStream; - return MaxFramerate.HasValue && !IsDirectStream - ? MaxFramerate - : stream?.ReferenceFrameRate; + return TargetVideoStream?.RefFrames; } - } - /// - /// Gets the audio sample rate that will be in the output stream. - /// - public double? TargetVideoLevel - { - get + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) { - if (IsDirectStream) - { - return TargetVideoStream?.Level; - } + return GetTargetRefFrames(videoCodec); + } - var targetVideoCodecs = TargetVideoCodec; - var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec)) - { - return GetTargetVideoLevel(videoCodec); - } + return TargetVideoStream?.RefFrames; + } + } - return TargetVideoStream?.Level; - } + /// + /// Gets the target framerate that will be in the output stream. + /// + /// The target framerate. + public float? TargetFramerate + { + get + { + var stream = TargetVideoStream; + return MaxFramerate.HasValue && !IsDirectStream + ? MaxFramerate + : stream?.ReferenceFrameRate; } + } - /// - /// Gets the audio sample rate that will be in the output stream. - /// - public int? TargetPacketLength + /// + /// Gets the target video level that will be in the output stream. + /// + /// The target video level. + public double? TargetVideoLevel + { + get { - get + if (IsDirectStream) { - var stream = TargetVideoStream; - return !IsDirectStream - ? null - : stream?.PacketLength; + return TargetVideoStream?.Level; } - } - /// - /// Gets the audio sample rate that will be in the output stream. - /// - public string? TargetVideoProfile - { - get + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) { - if (IsDirectStream) - { - return TargetVideoStream?.Profile; - } + return GetTargetVideoLevel(videoCodec); + } - var targetVideoCodecs = TargetVideoCodec; - var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec)) - { - return GetOption(videoCodec, "profile"); - } + return TargetVideoStream?.Level; + } + } - return TargetVideoStream?.Profile; - } + /// + /// Gets the target packet length that will be in the output stream. + /// + /// The target packet length. + public int? TargetPacketLength + { + get + { + var stream = TargetVideoStream; + return !IsDirectStream + ? null + : stream?.PacketLength; } + } - /// - /// Gets the target video range type that will be in the output stream. - /// - public VideoRangeType TargetVideoRangeType + /// + /// Gets the target video profile that will be in the output stream. + /// + /// The target video profile. + public string? TargetVideoProfile + { + get { - get + if (IsDirectStream) { - if (IsDirectStream) - { - return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown; - } - - var targetVideoCodecs = TargetVideoCodec; - var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec) - && Enum.TryParse(GetOption(videoCodec, "rangetype"), true, out VideoRangeType videoRangeType)) - { - return videoRangeType; - } + return TargetVideoStream?.Profile; + } - return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown; + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) + { + return GetOption(videoCodec, "profile"); } + + return TargetVideoStream?.Profile; } + } - /// - /// Gets the target video codec tag. - /// - /// The target video codec tag. - public string? TargetVideoCodecTag + /// + /// Gets the target video range type that will be in the output stream. + /// + /// The video range type. + public VideoRangeType TargetVideoRangeType + { + get { - get + if (IsDirectStream) { - var stream = TargetVideoStream; - return !IsDirectStream - ? null - : stream?.CodecTag; + return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown; } - } - /// - /// Gets the audio bitrate that will be in the output stream. - /// - public int? TargetAudioBitrate - { - get + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec) + && Enum.TryParse(GetOption(videoCodec, "rangetype"), true, out VideoRangeType videoRangeType)) { - var stream = TargetAudioStream; - return AudioBitrate.HasValue && !IsDirectStream - ? AudioBitrate - : stream?.BitRate; + return videoRangeType; } + + return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown; } + } - /// - /// Gets the audio channels that will be in the output stream. - /// - public int? TargetAudioChannels + /// + /// Gets the target video codec tag. + /// + /// The video codec tag. + public string? TargetVideoCodecTag + { + get { - get - { - if (IsDirectStream) - { - return TargetAudioStream?.Channels; - } + var stream = TargetVideoStream; + return !IsDirectStream + ? null + : stream?.CodecTag; + } + } - var targetAudioCodecs = TargetAudioCodec; - var codec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0]; - if (!string.IsNullOrEmpty(codec)) - { - return GetTargetRefFrames(codec); - } + /// + /// Gets the audio bitrate that will be in the output stream. + /// + /// The audio bitrate. + public int? TargetAudioBitrate + { + get + { + var stream = TargetAudioStream; + return AudioBitrate.HasValue && !IsDirectStream + ? AudioBitrate + : stream?.BitRate; + } + } + /// + /// Gets the amount of audio channels that will be in the output stream. + /// + /// The target audio channels. + public int? TargetAudioChannels + { + get + { + if (IsDirectStream) + { return TargetAudioStream?.Channels; } + + var targetAudioCodecs = TargetAudioCodec; + var codec = targetAudioCodecs.Count == 0 ? null : targetAudioCodecs[0]; + if (!string.IsNullOrEmpty(codec)) + { + return GetTargetRefFrames(codec); + } + + return TargetAudioStream?.Channels; } + } - /// - /// Gets the audio codec that will be in the output stream. - /// - public string[] TargetAudioCodec + /// + /// Gets the audio codec that will be in the output stream. + /// + /// The audio codec. + public IReadOnlyList TargetAudioCodec + { + get { - get - { - var stream = TargetAudioStream; + var stream = TargetAudioStream; - string? inputCodec = stream?.Codec; + string? inputCodec = stream?.Codec; - if (IsDirectStream) - { - return string.IsNullOrEmpty(inputCodec) ? Array.Empty() : new[] { inputCodec }; - } + if (IsDirectStream) + { + return string.IsNullOrEmpty(inputCodec) ? [] : [inputCodec]; + } - foreach (string codec in AudioCodecs) + foreach (string codec in AudioCodecs) + { + if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase)) - { - return string.IsNullOrEmpty(codec) ? Array.Empty() : new[] { codec }; - } + return string.IsNullOrEmpty(codec) ? [] : [codec]; } - - return AudioCodecs; } + + return AudioCodecs; } + } - public string[] TargetVideoCodec + /// + /// Gets the video codec that will be in the output stream. + /// + /// The target video codec. + public IReadOnlyList TargetVideoCodec + { + get { - get - { - var stream = TargetVideoStream; + var stream = TargetVideoStream; - string? inputCodec = stream?.Codec; + string? inputCodec = stream?.Codec; - if (IsDirectStream) - { - return string.IsNullOrEmpty(inputCodec) ? Array.Empty() : new[] { inputCodec }; - } + if (IsDirectStream) + { + return string.IsNullOrEmpty(inputCodec) ? [] : [inputCodec]; + } - foreach (string codec in VideoCodecs) + foreach (string codec in VideoCodecs) + { + if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase)) - { - return string.IsNullOrEmpty(codec) ? Array.Empty() : new[] { codec }; - } + return string.IsNullOrEmpty(codec) ? [] : [codec]; } - - return VideoCodecs; } + + return VideoCodecs; } + } - /// - /// Gets the audio channels that will be in the output stream. - /// - public long? TargetSize + /// + /// Gets the target size of the output stream. + /// + /// The target size. + public long? TargetSize + { + get { - get + if (IsDirectStream) { - if (IsDirectStream) - { - return MediaSource?.Size; - } - - if (RunTimeTicks.HasValue) - { - int? totalBitrate = TargetTotalBitrate; + return MediaSource?.Size; + } - double totalSeconds = RunTimeTicks.Value; - // Convert to ms - totalSeconds /= 10000; - // Convert to seconds - totalSeconds /= 1000; + if (RunTimeTicks.HasValue) + { + int? totalBitrate = TargetTotalBitrate; - return totalBitrate.HasValue ? - Convert.ToInt64(totalBitrate.Value * totalSeconds) : - null; - } + double totalSeconds = RunTimeTicks.Value; + // Convert to ms + totalSeconds /= 10000; + // Convert to seconds + totalSeconds /= 1000; - return null; + return totalBitrate.HasValue ? + Convert.ToInt64(totalBitrate.Value * totalSeconds) : + null; } + + return null; } + } - public int? TargetVideoBitrate + /// + /// Gets the target video bitrate of the output stream. + /// + /// The video bitrate. + public int? TargetVideoBitrate + { + get { - get - { - var stream = TargetVideoStream; + var stream = TargetVideoStream; - return VideoBitrate.HasValue && !IsDirectStream - ? VideoBitrate - : stream?.BitRate; - } + return VideoBitrate.HasValue && !IsDirectStream + ? VideoBitrate + : stream?.BitRate; } + } - public TransportStreamTimestamp TargetTimestamp + /// + /// Gets the target timestamp of the output stream. + /// + /// The target timestamp. + public TransportStreamTimestamp TargetTimestamp + { + get { - get - { - var defaultValue = string.Equals(Container, "m2ts", StringComparison.OrdinalIgnoreCase) - ? TransportStreamTimestamp.Valid - : TransportStreamTimestamp.None; + var defaultValue = string.Equals(Container, "m2ts", StringComparison.OrdinalIgnoreCase) + ? TransportStreamTimestamp.Valid + : TransportStreamTimestamp.None; - return !IsDirectStream - ? defaultValue - : MediaSource is null ? defaultValue : MediaSource.Timestamp ?? TransportStreamTimestamp.None; - } + return !IsDirectStream + ? defaultValue + : MediaSource is null ? defaultValue : MediaSource.Timestamp ?? TransportStreamTimestamp.None; } + } - public int? TargetTotalBitrate => (TargetAudioBitrate ?? 0) + (TargetVideoBitrate ?? 0); + /// + /// Gets the target total bitrate of the output stream. + /// + /// The target total bitrate. + public int? TargetTotalBitrate => (TargetAudioBitrate ?? 0) + (TargetVideoBitrate ?? 0); - public bool? IsTargetAnamorphic + /// + /// Gets a value indicating whether the output stream is anamorphic. + /// + public bool? IsTargetAnamorphic + { + get { - get + if (IsDirectStream) { - if (IsDirectStream) - { - return TargetVideoStream?.IsAnamorphic; - } - - return false; + return TargetVideoStream?.IsAnamorphic; } + + return false; } + } - public bool? IsTargetInterlaced + /// + /// Gets a value indicating whether the output stream is interlaced. + /// + public bool? IsTargetInterlaced + { + get { - get + if (IsDirectStream) { - if (IsDirectStream) - { - return TargetVideoStream?.IsInterlaced; - } - - var targetVideoCodecs = TargetVideoCodec; - var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec)) - { - if (string.Equals(GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - } - return TargetVideoStream?.IsInterlaced; } - } - public bool? IsTargetAVC - { - get + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) { - if (IsDirectStream) + if (string.Equals(GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase)) { - return TargetVideoStream?.IsAVC; + return false; } - - return true; } + + return TargetVideoStream?.IsInterlaced; } + } - public int? TargetWidth + /// + /// Gets a value indicating whether the output stream is AVC. + /// + public bool? IsTargetAVC + { + get { - get + if (IsDirectStream) { - var videoStream = TargetVideoStream; - - if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue) - { - ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value); - - size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0); - - return size.Width; - } - - return MaxWidth; + return TargetVideoStream?.IsAVC; } + + return true; } + } - public int? TargetHeight + /// + /// Gets the target width of the output stream. + /// + /// The target width. + public int? TargetWidth + { + get { - get - { - var videoStream = TargetVideoStream; - - if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue) - { - ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value); + var videoStream = TargetVideoStream; - size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0); + if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue) + { + ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value); - return size.Height; - } + size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0); - return MaxHeight; + return size.Width; } + + return MaxWidth; } + } - public int? TargetVideoStreamCount + /// + /// Gets the target height of the output stream. + /// + /// The target height. + public int? TargetHeight + { + get { - get + var videoStream = TargetVideoStream; + + if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue) { - if (IsDirectStream) - { - return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue); - } + ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value); - return GetMediaStreamCount(MediaStreamType.Video, 1); + size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0); + + return size.Height; } + + return MaxHeight; } + } - public int? TargetAudioStreamCount + /// + /// Gets the target video stream count of the output stream. + /// + /// The target video stream count. + public int? TargetVideoStreamCount + { + get { - get + if (IsDirectStream) { - if (IsDirectStream) - { - return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue); - } - - return GetMediaStreamCount(MediaStreamType.Audio, 1); + return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue); } + + return GetMediaStreamCount(MediaStreamType.Video, 1); } + } - public void SetOption(string? qualifier, string name, string value) + /// + /// Gets the target audio stream count of the output stream. + /// + /// The target audio stream count. + public int? TargetAudioStreamCount + { + get { - if (string.IsNullOrEmpty(qualifier)) + if (IsDirectStream) { - SetOption(name, value); - } - else - { - SetOption(qualifier + "-" + name, value); + return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue); } + + return GetMediaStreamCount(MediaStreamType.Audio, 1); } + } - public void SetOption(string name, string value) + /// + /// Sets a stream option. + /// + /// The qualifier. + /// The name. + /// The value. + public void SetOption(string? qualifier, string name, string value) + { + if (string.IsNullOrEmpty(qualifier)) { - StreamOptions[name] = value; + SetOption(name, value); } - - public string? GetOption(string? qualifier, string name) + else { - var value = GetOption(qualifier + "-" + name); + SetOption(qualifier + "-" + name, value); + } + } - if (string.IsNullOrEmpty(value)) - { - value = GetOption(name); - } + /// + /// Sets a stream option. + /// + /// The name. + /// The value. + public void SetOption(string name, string value) + { + StreamOptions[name] = value; + } - return value; - } + /// + /// Gets a stream option. + /// + /// The qualifier. + /// The name. + /// The value. + public string? GetOption(string? qualifier, string name) + { + var value = GetOption(qualifier + "-" + name); - public string? GetOption(string name) + if (string.IsNullOrEmpty(value)) { - if (StreamOptions.TryGetValue(name, out var value)) - { - return value; - } - - return null; + value = GetOption(name); } - public string ToUrl(string baseUrl, string? accessToken) + return value; + } + + /// + /// Gets a stream option. + /// + /// The name. + /// The value. + public string? GetOption(string name) + { + if (StreamOptions.TryGetValue(name, out var value)) { - ArgumentException.ThrowIfNullOrEmpty(baseUrl); + return value; + } - var list = new List(); - foreach (NameValuePair pair in BuildParams(this, accessToken)) - { - if (string.IsNullOrEmpty(pair.Value)) - { - continue; - } + return null; + } - // Try to keep the url clean by omitting defaults - if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase) - && string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase)) - { - continue; - } + /// + /// Returns this output stream URL for this class. + /// + /// The base Url. + /// The access Token. + /// A querystring representation of this object. + public string ToUrl(string baseUrl, string? accessToken) + { + ArgumentException.ThrowIfNullOrEmpty(baseUrl); - if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase) - && string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase)) - { - continue; - } + List list = []; + foreach (NameValuePair pair in BuildParams(this, accessToken)) + { + if (string.IsNullOrEmpty(pair.Value)) + { + continue; + } - if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase) - && string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase)) - { - continue; - } + // Try to keep the url clean by omitting defaults + if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase) + && string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase)) + { + continue; + } - var encodedValue = pair.Value.Replace(" ", "%20", StringComparison.Ordinal); + if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase) + && string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase)) + { + continue; + } - list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue)); + if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase) + && string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase)) + { + continue; } - string queryString = string.Join('&', list); + var encodedValue = pair.Value.Replace(" ", "%20", StringComparison.Ordinal); - return GetUrl(baseUrl, queryString); + list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue)); } - private string GetUrl(string baseUrl, string queryString) - { - ArgumentException.ThrowIfNullOrEmpty(baseUrl); + string queryString = string.Join('&', list); - string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container; + return GetUrl(baseUrl, queryString); + } - baseUrl = baseUrl.TrimEnd('/'); + private string GetUrl(string baseUrl, string queryString) + { + ArgumentException.ThrowIfNullOrEmpty(baseUrl); - if (MediaType == DlnaProfileType.Audio) - { - if (SubProtocol == MediaStreamProtocol.hls) - { - return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); - } + string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container; - return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); - } + baseUrl = baseUrl.TrimEnd('/'); + if (MediaType == DlnaProfileType.Audio) + { if (SubProtocol == MediaStreamProtocol.hls) { - return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); + return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); } - return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); + return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); } - private static IEnumerable BuildParams(StreamInfo item, string? accessToken) + if (SubProtocol == MediaStreamProtocol.hls) { - var list = new List(); + return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); + } - string audioCodecs = item.AudioCodecs.Length == 0 ? - string.Empty : - string.Join(',', item.AudioCodecs); + return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); + } - string videoCodecs = item.VideoCodecs.Length == 0 ? - string.Empty : - string.Join(',', item.VideoCodecs); + private static List BuildParams(StreamInfo item, string? accessToken) + { + List list = []; + + string audioCodecs = item.AudioCodecs.Count == 0 ? + string.Empty : + string.Join(',', item.AudioCodecs); + + string videoCodecs = item.VideoCodecs.Count == 0 ? + string.Empty : + string.Join(',', item.VideoCodecs); + + list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty)); + list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty)); + list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty)); + list.Add(new NameValuePair("Static", item.IsDirectStream.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + list.Add(new NameValuePair("VideoCodec", videoCodecs)); + list.Add(new NameValuePair("AudioCodec", audioCodecs)); + list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + + list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + + long startPositionTicks = item.StartPositionTicks; + + if (item.SubProtocol == MediaStreamProtocol.hls) + { + list.Add(new NameValuePair("StartTimeTicks", string.Empty)); + } + else + { + list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture))); + } - list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty)); - list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty)); - list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty)); - list.Add(new NameValuePair("Static", item.IsDirectStream.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - list.Add(new NameValuePair("VideoCodec", videoCodecs)); - list.Add(new NameValuePair("AudioCodec", audioCodecs)); - list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty)); + list.Add(new NameValuePair("api_key", accessToken ?? string.Empty)); - list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + string? liveStreamId = item.MediaSource?.LiveStreamId; + list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty)); - long startPositionTicks = item.StartPositionTicks; + list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty)); - if (item.SubProtocol == MediaStreamProtocol.hls) - { - list.Add(new NameValuePair("StartTimeTicks", string.Empty)); - } - else + if (!item.IsDirectStream) + { + if (item.RequireNonAnamorphic) { - list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture))); + list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); } - list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty)); - list.Add(new NameValuePair("api_key", accessToken ?? string.Empty)); - - string? liveStreamId = item.MediaSource?.LiveStreamId; - list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty)); + list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty)); - - if (!item.IsDirectStream) + if (item.EnableSubtitlesInManifest) { - if (item.RequireNonAnamorphic) - { - list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } - - list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - - if (item.EnableSubtitlesInManifest) - { - list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } - - if (item.EnableMpegtsM2TsMode) - { - list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } - - if (item.EstimateContentLength) - { - list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } + list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + } - if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto) - { - list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant())); - } + if (item.EnableMpegtsM2TsMode) + { + list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + } - if (item.CopyTimestamps) - { - list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } + if (item.EstimateContentLength) + { + list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + } - list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto) + { + list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant())); + } - list.Add(new NameValuePair("EnableAudioVbrEncoding", item.EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + if (item.CopyTimestamps) + { + list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); } - list.Add(new NameValuePair("Tag", item.MediaSource?.ETag ?? string.Empty)); + list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - string subtitleCodecs = item.SubtitleCodecs.Length == 0 ? - string.Empty : - string.Join(",", item.SubtitleCodecs); + list.Add(new NameValuePair("EnableAudioVbrEncoding", item.EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + } - list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty)); + list.Add(new NameValuePair("Tag", item.MediaSource?.ETag ?? string.Empty)); - if (item.SubProtocol == MediaStreamProtocol.hls) - { - list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty)); + string subtitleCodecs = item.SubtitleCodecs.Count == 0 ? + string.Empty : + string.Join(",", item.SubtitleCodecs); - if (item.SegmentLength.HasValue) - { - list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture))); - } + list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty)); - if (item.MinSegments.HasValue) - { - list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture))); - } + if (item.SubProtocol == MediaStreamProtocol.hls) + { + list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty)); - list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture))); + if (item.SegmentLength.HasValue) + { + list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture))); } - foreach (var pair in item.StreamOptions) + if (item.MinSegments.HasValue) { - if (string.IsNullOrEmpty(pair.Value)) - { - continue; - } - - // strip spaces to avoid having to encode h264 profile names - list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal))); + list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture))); } - if (!item.IsDirectStream) + list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture))); + } + + foreach (var pair in item.StreamOptions) + { + if (string.IsNullOrEmpty(pair.Value)) { - list.Add(new NameValuePair("TranscodeReasons", item.TranscodeReasons.ToString())); + continue; } - return list; + // strip spaces to avoid having to encode h264 profile names + list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal))); } - public IEnumerable GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string? accessToken) + if (!item.IsDirectStream) { - return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken); + list.Add(new NameValuePair("TranscodeReasons", item.TranscodeReasons.ToString())); } - public IEnumerable GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string? accessToken) + return list; + } + + /// + /// Gets the subtitle profiles. + /// + /// The transcoder support. + /// If only the selected track should be included. + /// The base URL. + /// The access token. + /// The of the profiles. + public IEnumerable GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string? accessToken) + { + return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken); + } + + /// + /// Gets the subtitle profiles. + /// + /// The transcoder support. + /// If only the selected track should be included. + /// If all profiles are enabled. + /// The base URL. + /// The access token. + /// The of the profiles. + public IEnumerable GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string? accessToken) + { + if (MediaSource is null) { - if (MediaSource is null) - { - return Enumerable.Empty(); - } + return []; + } - var list = new List(); + List list = []; - // HLS will preserve timestamps so we can just grab the full subtitle stream - long startPositionTicks = SubProtocol == MediaStreamProtocol.hls - ? 0 - : (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0); + // HLS will preserve timestamps so we can just grab the full subtitle stream + long startPositionTicks = SubProtocol == MediaStreamProtocol.hls + ? 0 + : (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0); - // First add the selected track - if (SubtitleStreamIndex.HasValue) + // First add the selected track + if (SubtitleStreamIndex.HasValue) + { + foreach (var stream in MediaSource.MediaStreams) { - foreach (var stream in MediaSource.MediaStreams) + if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value) { - if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value) - { - AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks); - } + AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks); } } + } - if (!includeSelectedTrackOnly) + if (!includeSelectedTrackOnly) + { + foreach (var stream in MediaSource.MediaStreams) { - foreach (var stream in MediaSource.MediaStreams) + if (stream.Type == MediaStreamType.Subtitle && (!SubtitleStreamIndex.HasValue || stream.Index != SubtitleStreamIndex.Value)) { - if (stream.Type == MediaStreamType.Subtitle && (!SubtitleStreamIndex.HasValue || stream.Index != SubtitleStreamIndex.Value)) - { - AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks); - } + AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks); } } - - return list; } - private void AddSubtitleProfiles(List list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string? accessToken, long startPositionTicks) + return list; + } + + private void AddSubtitleProfiles(List list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string? accessToken, long startPositionTicks) + { + if (enableAllProfiles) { - if (enableAllProfiles) - { - foreach (var profile in DeviceProfile.SubtitleProfiles) - { - var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport); - if (info is not null) - { - list.Add(info); - } - } - } - else + foreach (var profile in DeviceProfile.SubtitleProfiles) { - var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport); + var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport); if (info is not null) { list.Add(info); } } } - - private SubtitleStreamInfo? GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string? accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport) + else { - if (MediaSource is null) + var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport); + if (info is not null) { - return null; + list.Add(info); } + } + } - var subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol); - var info = new SubtitleStreamInfo - { - IsForced = stream.IsForced, - Language = stream.Language, - Name = stream.Language ?? "Unknown", - Format = subtitleProfile.Format, - Index = stream.Index, - DeliveryMethod = subtitleProfile.Method, - DisplayTitle = stream.DisplayTitle - }; + private SubtitleStreamInfo? GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string? accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport) + { + if (MediaSource is null) + { + return null; + } - if (info.DeliveryMethod == SubtitleDeliveryMethod.External) + var subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol); + var info = new SubtitleStreamInfo + { + IsForced = stream.IsForced, + Language = stream.Language, + Name = stream.Language ?? "Unknown", + Format = subtitleProfile.Format, + Index = stream.Index, + DeliveryMethod = subtitleProfile.Method, + DisplayTitle = stream.DisplayTitle + }; + + if (info.DeliveryMethod == SubtitleDeliveryMethod.External) + { + if (MediaSource.Protocol == MediaProtocol.File || !string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) || !stream.IsExternal) { - if (MediaSource.Protocol == MediaProtocol.File || !string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) || !stream.IsExternal) + info.Url = string.Format( + CultureInfo.InvariantCulture, + "{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}", + baseUrl, + ItemId, + MediaSourceId, + stream.Index.ToString(CultureInfo.InvariantCulture), + startPositionTicks.ToString(CultureInfo.InvariantCulture), + subtitleProfile.Format); + + if (!string.IsNullOrEmpty(accessToken)) { - info.Url = string.Format( - CultureInfo.InvariantCulture, - "{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}", - baseUrl, - ItemId, - MediaSourceId, - stream.Index.ToString(CultureInfo.InvariantCulture), - startPositionTicks.ToString(CultureInfo.InvariantCulture), - subtitleProfile.Format); - - if (!string.IsNullOrEmpty(accessToken)) - { - info.Url += "?api_key=" + accessToken; - } - - info.IsExternalUrl = false; + info.Url += "?api_key=" + accessToken; } - else - { - info.Url = stream.Path; - info.IsExternalUrl = true; - } - } - - return info; - } - - public int? GetTargetVideoBitDepth(string? codec) - { - var value = GetOption(codec, "videobitdepth"); - if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) + info.IsExternalUrl = false; + } + else { - return result; + info.Url = stream.Path; + info.IsExternalUrl = true; } - - return null; } - public int? GetTargetAudioBitDepth(string? codec) - { - var value = GetOption(codec, "audiobitdepth"); + return info; + } - if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) - { - return result; - } + /// + /// Gets the target video bit depth. + /// + /// The codec. + /// The target video bit depth. + public int? GetTargetVideoBitDepth(string? codec) + { + var value = GetOption(codec, "videobitdepth"); - return null; + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) + { + return result; } - public double? GetTargetVideoLevel(string? codec) - { - var value = GetOption(codec, "level"); + return null; + } - if (double.TryParse(value, CultureInfo.InvariantCulture, out var result)) - { - return result; - } + /// + /// Gets the target audio bit depth. + /// + /// The codec. + /// The target audio bit depth. + public int? GetTargetAudioBitDepth(string? codec) + { + var value = GetOption(codec, "audiobitdepth"); - return null; + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) + { + return result; } - public int? GetTargetRefFrames(string? codec) - { - var value = GetOption(codec, "maxrefframes"); + return null; + } - if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) - { - return result; - } + /// + /// Gets the target video level. + /// + /// The codec. + /// The target video level. + public double? GetTargetVideoLevel(string? codec) + { + var value = GetOption(codec, "level"); - return null; + if (double.TryParse(value, CultureInfo.InvariantCulture, out var result)) + { + return result; } - public int? GetTargetAudioChannels(string? codec) + return null; + } + + /// + /// Gets the target reference frames. + /// + /// The codec. + /// The target reference frames. + public int? GetTargetRefFrames(string? codec) + { + var value = GetOption(codec, "maxrefframes"); + + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { - var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels; + return result; + } - var value = GetOption(codec, "audiochannels"); - if (string.IsNullOrEmpty(value)) - { - return defaultValue; - } + return null; + } - if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) - { - return Math.Min(result, defaultValue ?? result); - } + /// + /// Gets the target audio channels. + /// + /// The codec. + /// The target audio channels. + public int? GetTargetAudioChannels(string? codec) + { + var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels; + var value = GetOption(codec, "audiochannels"); + if (string.IsNullOrEmpty(value)) + { return defaultValue; } - private int? GetMediaStreamCount(MediaStreamType type, int limit) + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { - var count = MediaSource?.GetStreamCount(type); + return Math.Min(result, defaultValue ?? result); + } - if (count.HasValue) - { - count = Math.Min(count.Value, limit); - } + return defaultValue; + } + + /// + /// Gets the media stream count. + /// + /// The type. + /// The limit. + /// The media stream count. + private int? GetMediaStreamCount(MediaStreamType type, int limit) + { + var count = MediaSource?.GetStreamCount(type); - return count; + if (count.HasValue) + { + count = Math.Min(count.Value, limit); } + + return count; } } diff --git a/MediaBrowser.Model/Dlna/SubtitleProfile.cs b/MediaBrowser.Model/Dlna/SubtitleProfile.cs index 9ebde25ffe..1879f2dd23 100644 --- a/MediaBrowser.Model/Dlna/SubtitleProfile.cs +++ b/MediaBrowser.Model/Dlna/SubtitleProfile.cs @@ -1,48 +1,62 @@ #nullable disable -#pragma warning disable CS1591 -using System; using System.Xml.Serialization; -using Jellyfin.Extensions; +using MediaBrowser.Model.Extensions; -namespace MediaBrowser.Model.Dlna +namespace MediaBrowser.Model.Dlna; + +/// +/// A class for subtitle profile information. +/// +public class SubtitleProfile { - public class SubtitleProfile + /// + /// Gets or sets the format. + /// + [XmlAttribute("format")] + public string Format { get; set; } + + /// + /// Gets or sets the delivery method. + /// + [XmlAttribute("method")] + public SubtitleDeliveryMethod Method { get; set; } + + /// + /// Gets or sets the DIDL mode. + /// + [XmlAttribute("didlMode")] + public string DidlMode { get; set; } + + /// + /// Gets or sets the language. + /// + [XmlAttribute("language")] + public string Language { get; set; } + + /// + /// Gets or sets the container. + /// + [XmlAttribute("container")] + public string Container { get; set; } + + /// + /// Checks if a language is supported. + /// + /// The language to check for support. + /// true if supported. + public bool SupportsLanguage(string subLanguage) { - [XmlAttribute("format")] - public string Format { get; set; } - - [XmlAttribute("method")] - public SubtitleDeliveryMethod Method { get; set; } - - [XmlAttribute("didlMode")] - public string DidlMode { get; set; } - - [XmlAttribute("language")] - public string Language { get; set; } - - [XmlAttribute("container")] - public string Container { get; set; } - - public string[] GetLanguages() + if (string.IsNullOrEmpty(Language)) { - return ContainerProfile.SplitValue(Language); + return true; } - public bool SupportsLanguage(string subLanguage) + if (string.IsNullOrEmpty(subLanguage)) { - if (string.IsNullOrEmpty(Language)) - { - return true; - } - - if (string.IsNullOrEmpty(subLanguage)) - { - subLanguage = "und"; - } - - var languages = GetLanguages(); - return languages.Length == 0 || languages.Contains(subLanguage, StringComparison.OrdinalIgnoreCase); + subLanguage = "und"; } + + return ContainerHelper.ContainsContainer(Language, subLanguage); } } diff --git a/MediaBrowser.Model/Dlna/TranscodingProfile.cs b/MediaBrowser.Model/Dlna/TranscodingProfile.cs index a556799deb..5a9fa22ae4 100644 --- a/MediaBrowser.Model/Dlna/TranscodingProfile.cs +++ b/MediaBrowser.Model/Dlna/TranscodingProfile.cs @@ -1,82 +1,130 @@ -#pragma warning disable CS1591 - -using System; using System.ComponentModel; using System.Xml.Serialization; using Jellyfin.Data.Enums; -namespace MediaBrowser.Model.Dlna +namespace MediaBrowser.Model.Dlna; + +/// +/// A class for transcoding profile information. +/// +public class TranscodingProfile { - public class TranscodingProfile + /// + /// Initializes a new instance of the class. + /// + public TranscodingProfile() { - public TranscodingProfile() - { - Conditions = Array.Empty(); - } - - [XmlAttribute("container")] - public string Container { get; set; } = string.Empty; - - [XmlAttribute("type")] - public DlnaProfileType Type { get; set; } - - [XmlAttribute("videoCodec")] - public string VideoCodec { get; set; } = string.Empty; - - [XmlAttribute("audioCodec")] - public string AudioCodec { get; set; } = string.Empty; - - [XmlAttribute("protocol")] - public MediaStreamProtocol Protocol { get; set; } = MediaStreamProtocol.http; - - [DefaultValue(false)] - [XmlAttribute("estimateContentLength")] - public bool EstimateContentLength { get; set; } - - [DefaultValue(false)] - [XmlAttribute("enableMpegtsM2TsMode")] - public bool EnableMpegtsM2TsMode { get; set; } - - [DefaultValue(TranscodeSeekInfo.Auto)] - [XmlAttribute("transcodeSeekInfo")] - public TranscodeSeekInfo TranscodeSeekInfo { get; set; } - - [DefaultValue(false)] - [XmlAttribute("copyTimestamps")] - public bool CopyTimestamps { get; set; } - - [DefaultValue(EncodingContext.Streaming)] - [XmlAttribute("context")] - public EncodingContext Context { get; set; } - - [DefaultValue(false)] - [XmlAttribute("enableSubtitlesInManifest")] - public bool EnableSubtitlesInManifest { get; set; } - - [XmlAttribute("maxAudioChannels")] - public string? MaxAudioChannels { get; set; } - - [DefaultValue(0)] - [XmlAttribute("minSegments")] - public int MinSegments { get; set; } - - [DefaultValue(0)] - [XmlAttribute("segmentLength")] - public int SegmentLength { get; set; } - - [DefaultValue(false)] - [XmlAttribute("breakOnNonKeyFrames")] - public bool BreakOnNonKeyFrames { get; set; } - - public ProfileCondition[] Conditions { get; set; } - - [DefaultValue(true)] - [XmlAttribute("enableAudioVbrEncoding")] - public bool EnableAudioVbrEncoding { get; set; } = true; - - public string[] GetAudioCodecs() - { - return ContainerProfile.SplitValue(AudioCodec); - } + Conditions = []; } + + /// + /// Gets or sets the container. + /// + [XmlAttribute("container")] + public string Container { get; set; } = string.Empty; + + /// + /// Gets or sets the DLNA profile type. + /// + [XmlAttribute("type")] + public DlnaProfileType Type { get; set; } + + /// + /// Gets or sets the video codec. + /// + [XmlAttribute("videoCodec")] + public string VideoCodec { get; set; } = string.Empty; + + /// + /// Gets or sets the audio codec. + /// + [XmlAttribute("audioCodec")] + public string AudioCodec { get; set; } = string.Empty; + + /// + /// Gets or sets the protocol. + /// + [XmlAttribute("protocol")] + public MediaStreamProtocol Protocol { get; set; } = MediaStreamProtocol.http; + + /// + /// Gets or sets a value indicating whether the content length should be estimated. + /// + [DefaultValue(false)] + [XmlAttribute("estimateContentLength")] + public bool EstimateContentLength { get; set; } + + /// + /// Gets or sets a value indicating whether M2TS mode is enabled. + /// + [DefaultValue(false)] + [XmlAttribute("enableMpegtsM2TsMode")] + public bool EnableMpegtsM2TsMode { get; set; } + + /// + /// Gets or sets the transcoding seek info mode. + /// + [DefaultValue(TranscodeSeekInfo.Auto)] + [XmlAttribute("transcodeSeekInfo")] + public TranscodeSeekInfo TranscodeSeekInfo { get; set; } + + /// + /// Gets or sets a value indicating whether timestamps should be copied. + /// + [DefaultValue(false)] + [XmlAttribute("copyTimestamps")] + public bool CopyTimestamps { get; set; } + + /// + /// Gets or sets the encoding context. + /// + [DefaultValue(EncodingContext.Streaming)] + [XmlAttribute("context")] + public EncodingContext Context { get; set; } + + /// + /// Gets or sets a value indicating whether subtitles are allowed in the manifest. + /// + [DefaultValue(false)] + [XmlAttribute("enableSubtitlesInManifest")] + public bool EnableSubtitlesInManifest { get; set; } + + /// + /// Gets or sets the maximum audio channels. + /// + [XmlAttribute("maxAudioChannels")] + public string? MaxAudioChannels { get; set; } + + /// + /// Gets or sets the minimum amount of segments. + /// + [DefaultValue(0)] + [XmlAttribute("minSegments")] + public int MinSegments { get; set; } + + /// + /// Gets or sets the segment length. + /// + [DefaultValue(0)] + [XmlAttribute("segmentLength")] + public int SegmentLength { get; set; } + + /// + /// Gets or sets a value indicating whether breaking the video stream on non-keyframes is supported. + /// + [DefaultValue(false)] + [XmlAttribute("breakOnNonKeyFrames")] + public bool BreakOnNonKeyFrames { get; set; } + + /// + /// Gets or sets the profile conditions. + /// + public ProfileCondition[] Conditions { get; set; } + + /// + /// Gets or sets a value indicating whether variable bitrate encoding is supported. + /// + [DefaultValue(true)] + [XmlAttribute("enableAudioVbrEncoding")] + public bool EnableAudioVbrEncoding { get; set; } = true; } diff --git a/MediaBrowser.Model/Extensions/ContainerHelper.cs b/MediaBrowser.Model/Extensions/ContainerHelper.cs new file mode 100644 index 0000000000..4b75657ff8 --- /dev/null +++ b/MediaBrowser.Model/Extensions/ContainerHelper.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using Jellyfin.Extensions; + +namespace MediaBrowser.Model.Extensions; + +/// +/// Defines the class. +/// +public static class ContainerHelper +{ + /// + /// Compares two containers, returning true if an item in exists + /// in . + /// + /// The comma-delimited string being searched. + /// If the parameter begins with the - character, the operation is reversed. + /// The comma-delimited string being matched. + /// The result of the operation. + public static bool ContainsContainer(string? profileContainers, string? inputContainer) + { + var isNegativeList = false; + if (profileContainers != null && profileContainers.StartsWith('-')) + { + isNegativeList = true; + profileContainers = profileContainers[1..]; + } + + return ContainsContainer(profileContainers, isNegativeList, inputContainer); + } + + /// + /// Compares two containers, returning true if an item in exists + /// in . + /// + /// The comma-delimited string being searched. + /// If the parameter begins with the - character, the operation is reversed. + /// The comma-delimited string being matched. + /// The result of the operation. + public static bool ContainsContainer(string? profileContainers, ReadOnlySpan inputContainer) + { + var isNegativeList = false; + if (profileContainers != null && profileContainers.StartsWith('-')) + { + isNegativeList = true; + profileContainers = profileContainers[1..]; + } + + return ContainsContainer(profileContainers, isNegativeList, inputContainer); + } + + /// + /// Compares two containers, returning if an item in + /// does not exist in . + /// + /// The comma-delimited string being searched. + /// The boolean result to return if a match is not found. + /// The comma-delimited string being matched. + /// The result of the operation. + public static bool ContainsContainer(string? profileContainers, bool isNegativeList, string? inputContainer) + { + if (string.IsNullOrEmpty(inputContainer)) + { + return isNegativeList; + } + + return ContainsContainer(profileContainers, isNegativeList, inputContainer.AsSpan()); + } + + /// + /// Compares two containers, returning if an item in + /// does not exist in . + /// + /// The comma-delimited string being searched. + /// The boolean result to return if a match is not found. + /// The comma-delimited string being matched. + /// The result of the operation. + public static bool ContainsContainer(string? profileContainers, bool isNegativeList, ReadOnlySpan inputContainer) + { + if (string.IsNullOrEmpty(profileContainers)) + { + // Empty profiles always support all containers/codecs. + return true; + } + + var allInputContainers = inputContainer.Split(','); + var allProfileContainers = profileContainers.SpanSplit(','); + foreach (var container in allInputContainers) + { + if (!container.IsEmpty) + { + foreach (var profile in allProfileContainers) + { + if (container.Equals(profile, StringComparison.OrdinalIgnoreCase)) + { + return !isNegativeList; + } + } + } + } + + return isNegativeList; + } + + /// + /// Compares two containers, returning if an item in + /// does not exist in . + /// + /// The profile containers being matched searched. + /// The boolean result to return if a match is not found. + /// The comma-delimited string being matched. + /// The result of the operation. + public static bool ContainsContainer(IReadOnlyList? profileContainers, bool isNegativeList, string inputContainer) + { + if (profileContainers is null) + { + // Empty profiles always support all containers/codecs. + return true; + } + + var allInputContainers = inputContainer.Split(','); + foreach (var container in allInputContainers) + { + foreach (var profile in profileContainers) + { + if (string.Equals(profile, container, StringComparison.OrdinalIgnoreCase)) + { + return !isNegativeList; + } + } + } + + return isNegativeList; + } + + /// + /// Splits and input string. + /// + /// The input string. + /// The result of the operation. + public static string[] Split(string? input) + { + return input?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? []; + } +} diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 51ac558b86..80bb1a514c 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -316,7 +316,7 @@ namespace MediaBrowser.Providers.MediaInfo genres = genres.SelectMany(g => SplitWithCustomDelimiter(g, libraryOptions.CustomTagDelimiters, libraryOptions.DelimiterWhitelist)).ToArray(); } - audio.Genres = options.ReplaceAllMetadata || audio.Genres == null || audio.Genres.Length == 0 + audio.Genres = options.ReplaceAllMetadata || audio.Genres is null || audio.Genres.Length == 0 ? genres : audio.Genres; } diff --git a/tests/Jellyfin.Model.Tests/Dlna/ContainerHelperTests.cs b/tests/Jellyfin.Model.Tests/Dlna/ContainerHelperTests.cs new file mode 100644 index 0000000000..68f8d94c72 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Dlna/ContainerHelperTests.cs @@ -0,0 +1,54 @@ +using System; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Extensions; +using Xunit; + +namespace Jellyfin.Model.Tests.Dlna; + +public class ContainerHelperTests +{ + private readonly ContainerProfile _emptyContainerProfile = new ContainerProfile(); + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("mp4")] + public void ContainsContainer_EmptyContainerProfile_ReturnsTrue(string? containers) + { + Assert.True(_emptyContainerProfile.ContainsContainer(containers)); + } + + [Theory] + [InlineData("mp3,mpeg", "mp3")] + [InlineData("mp3,mpeg,avi", "mp3,avi")] + [InlineData("-mp3,mpeg", "avi")] + [InlineData("-mp3,mpeg,avi", "mp4,jpg")] + public void ContainsContainer_InList_ReturnsTrue(string container, string? extension) + { + Assert.True(ContainerHelper.ContainsContainer(container, extension)); + } + + [Theory] + [InlineData("mp3,mpeg", "avi")] + [InlineData("mp3,mpeg,avi", "mp4,jpg")] + [InlineData("mp3,mpeg", null)] + [InlineData("mp3,mpeg", "")] + [InlineData("-mp3,mpeg", "mp3")] + [InlineData("-mp3,mpeg,avi", "mpeg,avi")] + [InlineData(",mp3,", ",avi,")] // Empty values should be discarded + [InlineData("-,mp3,", ",mp3,")] // Empty values should be discarded + public void ContainsContainer_NotInList_ReturnsFalse(string container, string? extension) + { + Assert.False(ContainerHelper.ContainsContainer(container, extension)); + } + + [Theory] + [InlineData("mp3,mpeg", "mp3")] + [InlineData("mp3,mpeg,avi", "mp3,avi")] + [InlineData("-mp3,mpeg", "avi")] + [InlineData("-mp3,mpeg,avi", "mp4,jpg")] + public void ContainsContainer_InList_ReturnsTrue_SpanVersion(string container, string? extension) + { + Assert.True(ContainerHelper.ContainsContainer(container, extension.AsSpan())); + } +} diff --git a/tests/Jellyfin.Model.Tests/Dlna/ContainerProfileTests.cs b/tests/Jellyfin.Model.Tests/Dlna/ContainerProfileTests.cs deleted file mode 100644 index cca056c280..0000000000 --- a/tests/Jellyfin.Model.Tests/Dlna/ContainerProfileTests.cs +++ /dev/null @@ -1,19 +0,0 @@ -using MediaBrowser.Model.Dlna; -using Xunit; - -namespace Jellyfin.Model.Tests.Dlna -{ - public class ContainerProfileTests - { - private readonly ContainerProfile _emptyContainerProfile = new ContainerProfile(); - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("mp4")] - public void ContainsContainer_EmptyContainerProfile_True(string? containers) - { - Assert.True(_emptyContainerProfile.ContainsContainer(containers)); - } - } -} diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index 7b4bb05ff1..bd2143f252 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -389,18 +389,23 @@ namespace Jellyfin.Model.Tests if (playMethod == PlayMethod.DirectPlay) { // Check expected container - var containers = ContainerProfile.SplitValue(mediaSource.Container); + var containers = mediaSource.Container.Split(','); + Assert.Contains(uri.Extension, containers); // TODO: Test transcode too - // Assert.Contains(uri.Extension, containers); // Check expected video codec (1) - Assert.Contains(targetVideoStream?.Codec, streamInfo.TargetVideoCodec); - Assert.Single(streamInfo.TargetVideoCodec); + if (targetVideoStream?.Codec is not null) + { + Assert.Contains(targetVideoStream?.Codec, streamInfo.TargetVideoCodec); + Assert.Single(streamInfo.TargetVideoCodec); + } - // Check expected audio codecs (1) - Assert.Contains(targetAudioStream?.Codec, streamInfo.TargetAudioCodec); - Assert.Single(streamInfo.TargetAudioCodec); - // Assert.Single(val.AudioCodecs); + if (targetAudioStream?.Codec is not null) + { + // Check expected audio codecs (1) + Assert.Contains(targetAudioStream?.Codec, streamInfo.TargetAudioCodec); + Assert.Single(streamInfo.TargetAudioCodec); + } if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal)) { -- cgit v1.2.3 From 7a2427bf07f9036d62c88a75855cd6dc7e8e3064 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 5 Sep 2024 12:55:15 +0200 Subject: Add SessionInfoDto, DeviceInfoDto and implement JsonDelimitedArrayConverter.Write --- .../Session/SessionManager.cs | 137 ++++++++++++++- Jellyfin.Api/Controllers/DevicesController.cs | 10 +- Jellyfin.Api/Controllers/SessionController.cs | 85 ++-------- .../Models/SessionDtos/ClientCapabilitiesDto.cs | 83 --------- Jellyfin.Data/Dtos/DeviceOptionsDto.cs | 33 ++-- .../Devices/DeviceManager.cs | 85 ++++++++-- .../Authentication/AuthenticationResult.cs | 33 ++-- MediaBrowser.Controller/Devices/IDeviceManager.cs | 150 ++++++++++------- .../AuthenticationResultEventArgs.cs | 3 +- .../WebSocketMessages/Outbound/SessionsMessage.cs | 5 +- MediaBrowser.Controller/Session/ISessionManager.cs | 11 ++ MediaBrowser.Controller/Session/SessionInfo.cs | 120 +++++++++++-- MediaBrowser.Model/Devices/DeviceInfo.cs | 119 +++++++------ MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs | 69 ++++++++ MediaBrowser.Model/Dto/DeviceInfoDto.cs | 83 +++++++++ MediaBrowser.Model/Dto/SessionInfoDto.cs | 186 +++++++++++++++++++++ .../Json/Converters/JsonDelimitedArrayConverter.cs | 65 ++++--- .../Converters/JsonCommaDelimitedArrayTests.cs | 16 +- 18 files changed, 919 insertions(+), 374 deletions(-) delete mode 100644 Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs create mode 100644 MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs create mode 100644 MediaBrowser.Model/Dto/DeviceInfoDto.cs create mode 100644 MediaBrowser.Model/Dto/SessionInfoDto.cs (limited to 'Jellyfin.Server.Implementations') diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 72e164b521..6bcbe3ceba 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -68,13 +66,29 @@ namespace Emby.Server.Implementations.Session private Timer _inactiveTimer; private DtoOptions _itemInfoDtoOptions; - private bool _disposed = false; + private bool _disposed; + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. public SessionManager( ILogger logger, IEventManager eventManager, IUserDataManager userDataManager, - IServerConfigurationManager config, + IServerConfigurationManager serverConfigurationManager, ILibraryManager libraryManager, IUserManager userManager, IMusicManager musicManager, @@ -88,7 +102,7 @@ namespace Emby.Server.Implementations.Session _logger = logger; _eventManager = eventManager; _userDataManager = userDataManager; - _config = config; + _config = serverConfigurationManager; _libraryManager = libraryManager; _userManager = userManager; _musicManager = musicManager; @@ -508,7 +522,10 @@ namespace Emby.Server.Implementations.Session deviceName = "Network Device"; } - var deviceOptions = _deviceManager.GetDeviceOptions(deviceId); + var deviceOptions = _deviceManager.GetDeviceOptions(deviceId) ?? new() + { + DeviceId = deviceId + }; if (string.IsNullOrEmpty(deviceOptions.CustomName)) { sessionInfo.DeviceName = deviceName; @@ -1076,6 +1093,42 @@ namespace Emby.Server.Implementations.Session return session; } + private SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo) + { + return new SessionInfoDto + { + PlayState = sessionInfo.PlayState, + AdditionalUsers = sessionInfo.AdditionalUsers, + Capabilities = _deviceManager.ToClientCapabilitiesDto(sessionInfo.Capabilities), + RemoteEndPoint = sessionInfo.RemoteEndPoint, + PlayableMediaTypes = sessionInfo.PlayableMediaTypes, + Id = sessionInfo.Id, + UserId = sessionInfo.UserId, + UserName = sessionInfo.UserName, + Client = sessionInfo.Client, + LastActivityDate = sessionInfo.LastActivityDate, + LastPlaybackCheckIn = sessionInfo.LastPlaybackCheckIn, + LastPausedDate = sessionInfo.LastPausedDate, + DeviceName = sessionInfo.DeviceName, + DeviceType = sessionInfo.DeviceType, + NowPlayingItem = sessionInfo.NowPlayingItem, + NowViewingItem = sessionInfo.NowViewingItem, + DeviceId = sessionInfo.DeviceId, + ApplicationVersion = sessionInfo.ApplicationVersion, + TranscodingInfo = sessionInfo.TranscodingInfo, + IsActive = sessionInfo.IsActive, + SupportsMediaControl = sessionInfo.SupportsMediaControl, + SupportsRemoteControl = sessionInfo.SupportsRemoteControl, + NowPlayingQueue = sessionInfo.NowPlayingQueue, + NowPlayingQueueFullItems = sessionInfo.NowPlayingQueueFullItems, + HasCustomDeviceName = sessionInfo.HasCustomDeviceName, + PlaylistItemId = sessionInfo.PlaylistItemId, + ServerId = sessionInfo.ServerId, + UserPrimaryImageTag = sessionInfo.UserPrimaryImageTag, + SupportedCommands = sessionInfo.SupportedCommands + }; + } + /// public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, CancellationToken cancellationToken) { @@ -1393,7 +1446,7 @@ namespace Emby.Server.Implementations.Session UserName = user.Username }; - session.AdditionalUsers = [..session.AdditionalUsers, newUser]; + session.AdditionalUsers = [.. session.AdditionalUsers, newUser]; } } @@ -1505,7 +1558,7 @@ namespace Emby.Server.Implementations.Session var returnResult = new AuthenticationResult { User = _userManager.GetUserDto(user, request.RemoteEndPoint), - SessionInfo = session, + SessionInfo = ToSessionInfoDto(session), AccessToken = token, ServerId = _appHost.SystemId }; @@ -1800,6 +1853,74 @@ namespace Emby.Server.Implementations.Session return await GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null).ConfigureAwait(false); } + /// + public IReadOnlyList GetSessions( + Guid userId, + string deviceId, + int? activeWithinSeconds, + Guid? controllableUserToCheck) + { + var result = Sessions; + var user = _userManager.GetUserById(userId); + if (!string.IsNullOrEmpty(deviceId)) + { + result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)); + } + + if (!controllableUserToCheck.IsNullOrEmpty()) + { + result = result.Where(i => i.SupportsRemoteControl); + + var controlledUser = _userManager.GetUserById(controllableUserToCheck.Value); + if (controlledUser is null) + { + return []; + } + + if (!controlledUser.HasPermission(PermissionKind.EnableSharedDeviceControl)) + { + // Controlled user has device sharing disabled + result = result.Where(i => !i.UserId.IsEmpty()); + } + + if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)) + { + // User cannot control other user's sessions, validate user id. + result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(controllableUserToCheck.Value)); + } + + result = result.Where(i => + { + if (!string.IsNullOrWhiteSpace(i.DeviceId) && !_deviceManager.CanAccessDevice(user, i.DeviceId)) + { + return false; + } + + return true; + }); + } + else if (!user.HasPermission(PermissionKind.IsAdministrator)) + { + // Request isn't from administrator, limit to "own" sessions. + result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId)); + + // Don't report acceleration type for non-admin users. + result = result.Select(r => + { + r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none; + return r; + }); + } + + if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0) + { + var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value); + result = result.Where(i => i.LastActivityDate >= minActiveDate); + } + + return result.Select(ToSessionInfoDto).ToList(); + } + /// public Task SendMessageToAdminSessions(SessionMessageType name, T data, CancellationToken cancellationToken) { diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 2a2ab4ad16..50050262f0 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -1,15 +1,13 @@ using System; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; -using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Data.Dtos; -using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Queries; using MediaBrowser.Common.Api; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Devices; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -47,7 +45,7 @@ public class DevicesController : BaseJellyfinApiController /// An containing the list of devices. [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetDevices([FromQuery] Guid? userId) + public ActionResult> GetDevices([FromQuery] Guid? userId) { userId = RequestHelpers.GetUserId(User, userId); return _deviceManager.GetDevicesForUser(userId); @@ -63,7 +61,7 @@ public class DevicesController : BaseJellyfinApiController [HttpGet("Info")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetDeviceInfo([FromQuery, Required] string id) + public ActionResult GetDeviceInfo([FromQuery, Required] string id) { var deviceInfo = _deviceManager.GetDevice(id); if (deviceInfo is null) @@ -84,7 +82,7 @@ public class DevicesController : BaseJellyfinApiController [HttpGet("Options")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetDeviceOptions([FromQuery, Required] string id) + public ActionResult GetDeviceOptions([FromQuery, Required] string id) { var deviceInfo = _deviceManager.GetDeviceOptions(id); if (deviceInfo is null) diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 942bdeb9e8..91a879b8ed 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -1,18 +1,13 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Api.Models.SessionDtos; using Jellyfin.Data.Enums; -using Jellyfin.Extensions; using MediaBrowser.Common.Api; -using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; @@ -32,22 +27,18 @@ public class SessionController : BaseJellyfinApiController { private readonly ISessionManager _sessionManager; private readonly IUserManager _userManager; - private readonly IDeviceManager _deviceManager; /// /// Initializes a new instance of the class. /// /// Instance of interface. /// Instance of interface. - /// Instance of interface. public SessionController( ISessionManager sessionManager, - IUserManager userManager, - IDeviceManager deviceManager) + IUserManager userManager) { _sessionManager = sessionManager; _userManager = userManager; - _deviceManager = deviceManager; } /// @@ -57,77 +48,25 @@ public class SessionController : BaseJellyfinApiController /// Filter by device Id. /// Optional. Filter by sessions that were active in the last n seconds. /// List of sessions returned. - /// An with the available sessions. + /// An with the available sessions. [HttpGet("Sessions")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetSessions( + public ActionResult> GetSessions( [FromQuery] Guid? controllableByUserId, [FromQuery] string? deviceId, [FromQuery] int? activeWithinSeconds) { - var result = _sessionManager.Sessions; - var isRequestingFromAdmin = User.IsInRole(UserRoles.Administrator); - - if (!string.IsNullOrEmpty(deviceId)) - { - result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)); - } - - if (!controllableByUserId.IsNullOrEmpty()) + Guid? controllableUserToCheck = controllableByUserId is null ? null : RequestHelpers.GetUserId(User, controllableByUserId); + var result = _sessionManager.GetSessions( + User.GetUserId(), + deviceId, + activeWithinSeconds, + controllableUserToCheck); + + if (result.Count == 0) { - result = result.Where(i => i.SupportsRemoteControl); - - var user = _userManager.GetUserById(controllableByUserId.Value); - if (user is null) - { - return NotFound(); - } - - if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)) - { - // User cannot control other user's sessions, validate user id. - result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(RequestHelpers.GetUserId(User, controllableByUserId))); - } - - if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl)) - { - result = result.Where(i => !i.UserId.IsEmpty()); - } - - result = result.Where(i => - { - if (!string.IsNullOrWhiteSpace(i.DeviceId)) - { - if (!_deviceManager.CanAccessDevice(user, i.DeviceId)) - { - return false; - } - } - - return true; - }); - } - else if (!isRequestingFromAdmin) - { - // Request isn't from administrator, limit to "own" sessions. - result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(User.GetUserId())); - } - - if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0) - { - var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value); - result = result.Where(i => i.LastActivityDate >= minActiveDate); - } - - // Request isn't from administrator, don't report acceleration type. - if (!isRequestingFromAdmin) - { - result = result.Select(r => - { - r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none; - return r; - }); + return NotFound(); } return Ok(result); diff --git a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs deleted file mode 100644 index c699c469d9..0000000000 --- a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Text.Json.Serialization; -using Jellyfin.Data.Enums; -using Jellyfin.Extensions.Json.Converters; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Session; - -namespace Jellyfin.Api.Models.SessionDtos; - -/// -/// Client capabilities dto. -/// -public class ClientCapabilitiesDto -{ - /// - /// Gets or sets the list of playable media types. - /// - [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList PlayableMediaTypes { get; set; } = Array.Empty(); - - /// - /// Gets or sets the list of supported commands. - /// - [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList SupportedCommands { get; set; } = Array.Empty(); - - /// - /// Gets or sets a value indicating whether session supports media control. - /// - public bool SupportsMediaControl { get; set; } - - /// - /// Gets or sets a value indicating whether session supports a persistent identifier. - /// - public bool SupportsPersistentIdentifier { get; set; } - - /// - /// Gets or sets the device profile. - /// - public DeviceProfile? DeviceProfile { get; set; } - - /// - /// Gets or sets the app store url. - /// - public string? AppStoreUrl { get; set; } - - /// - /// Gets or sets the icon url. - /// - public string? IconUrl { get; set; } - -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - // TODO: Remove after 10.9 - [Obsolete("Unused")] - [DefaultValue(false)] - public bool? SupportsContentUploading { get; set; } = false; - - // TODO: Remove after 10.9 - [Obsolete("Unused")] - [DefaultValue(false)] - public bool? SupportsSync { get; set; } = false; -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member - - /// - /// Convert the dto to the full model. - /// - /// The converted model. - public ClientCapabilities ToClientCapabilities() - { - return new ClientCapabilities - { - PlayableMediaTypes = PlayableMediaTypes, - SupportedCommands = SupportedCommands, - SupportsMediaControl = SupportsMediaControl, - SupportsPersistentIdentifier = SupportsPersistentIdentifier, - DeviceProfile = DeviceProfile, - AppStoreUrl = AppStoreUrl, - IconUrl = IconUrl - }; - } -} diff --git a/Jellyfin.Data/Dtos/DeviceOptionsDto.cs b/Jellyfin.Data/Dtos/DeviceOptionsDto.cs index 392ef5ff4e..aad5787097 100644 --- a/Jellyfin.Data/Dtos/DeviceOptionsDto.cs +++ b/Jellyfin.Data/Dtos/DeviceOptionsDto.cs @@ -1,23 +1,22 @@ -namespace Jellyfin.Data.Dtos +namespace Jellyfin.Data.Dtos; + +/// +/// A dto representing custom options for a device. +/// +public class DeviceOptionsDto { /// - /// A dto representing custom options for a device. + /// Gets or sets the id. /// - public class DeviceOptionsDto - { - /// - /// Gets or sets the id. - /// - public int Id { get; set; } + public int Id { get; set; } - /// - /// Gets or sets the device id. - /// - public string? DeviceId { get; set; } + /// + /// Gets or sets the device id. + /// + public string? DeviceId { get; set; } - /// - /// Gets or sets the custom name. - /// - public string? CustomName { get; set; } - } + /// + /// Gets or sets the custom name. + /// + public string? CustomName { get; set; } } diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index 415c04bbf1..d3bff2936c 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Jellyfin.Data.Dtos; using Jellyfin.Data.Entities; using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Enums; @@ -13,6 +14,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Devices; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; using Microsoft.EntityFrameworkCore; @@ -68,7 +70,7 @@ namespace Jellyfin.Server.Implementations.Devices } /// - public async Task UpdateDeviceOptions(string deviceId, string deviceName) + public async Task UpdateDeviceOptions(string deviceId, string? deviceName) { DeviceOptions? deviceOptions; var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); @@ -105,29 +107,37 @@ namespace Jellyfin.Server.Implementations.Devices } /// - public DeviceOptions GetDeviceOptions(string deviceId) + public DeviceOptionsDto? GetDeviceOptions(string deviceId) { - _deviceOptions.TryGetValue(deviceId, out var deviceOptions); + if (_deviceOptions.TryGetValue(deviceId, out var deviceOptions)) + { + return ToDeviceOptionsDto(deviceOptions); + } - return deviceOptions ?? new DeviceOptions(deviceId); + return null; } /// - public ClientCapabilities GetCapabilities(string deviceId) + public ClientCapabilities GetCapabilities(string? deviceId) { + if (deviceId is null) + { + return new(); + } + return _capabilitiesMap.TryGetValue(deviceId, out ClientCapabilities? result) ? result - : new ClientCapabilities(); + : new(); } /// - public DeviceInfo? GetDevice(string id) + public DeviceInfoDto? GetDevice(string id) { var device = _devices.Values.Where(d => d.DeviceId == id).OrderByDescending(d => d.DateLastActivity).FirstOrDefault(); _deviceOptions.TryGetValue(id, out var deviceOption); var deviceInfo = device is null ? null : ToDeviceInfo(device, deviceOption); - return deviceInfo; + return deviceInfo is null ? null : ToDeviceInfoDto(deviceInfo); } /// @@ -166,7 +176,7 @@ namespace Jellyfin.Server.Implementations.Devices } /// - public QueryResult GetDevicesForUser(Guid? userId) + public QueryResult GetDevicesForUser(Guid? userId) { IEnumerable devices = _devices.Values .OrderByDescending(d => d.DateLastActivity) @@ -187,9 +197,11 @@ namespace Jellyfin.Server.Implementations.Devices { _deviceOptions.TryGetValue(device.DeviceId, out var option); return ToDeviceInfo(device, option); - }).ToArray(); + }) + .Select(ToDeviceInfoDto) + .ToArray(); - return new QueryResult(array); + return new QueryResult(array); } /// @@ -235,13 +247,9 @@ namespace Jellyfin.Server.Implementations.Devices private DeviceInfo ToDeviceInfo(Device authInfo, DeviceOptions? options = null) { var caps = GetCapabilities(authInfo.DeviceId); - var user = _userManager.GetUserById(authInfo.UserId); - if (user is null) - { - throw new ResourceNotFoundException("User with UserId " + authInfo.UserId + " not found"); - } + var user = _userManager.GetUserById(authInfo.UserId) ?? throw new ResourceNotFoundException("User with UserId " + authInfo.UserId + " not found"); - return new DeviceInfo + return new() { AppName = authInfo.AppName, AppVersion = authInfo.AppVersion, @@ -254,5 +262,48 @@ namespace Jellyfin.Server.Implementations.Devices CustomName = options?.CustomName, }; } + + private DeviceOptionsDto ToDeviceOptionsDto(DeviceOptions options) + { + return new() + { + Id = options.Id, + DeviceId = options.DeviceId, + CustomName = options.CustomName, + }; + } + + private DeviceInfoDto ToDeviceInfoDto(DeviceInfo info) + { + return new() + { + Name = info.Name, + CustomName = info.CustomName, + AccessToken = info.AccessToken, + Id = info.Id, + LastUserName = info.LastUserName, + AppName = info.AppName, + AppVersion = info.AppVersion, + LastUserId = info.LastUserId, + DateLastActivity = info.DateLastActivity, + Capabilities = ToClientCapabilitiesDto(info.Capabilities), + IconUrl = info.IconUrl + }; + } + + /// + public ClientCapabilitiesDto ToClientCapabilitiesDto(ClientCapabilities capabilities) + { + return new() + { + PlayableMediaTypes = capabilities.PlayableMediaTypes, + SupportedCommands = capabilities.SupportedCommands, + SupportsMediaControl = capabilities.SupportsMediaControl, + SupportsPersistentIdentifier = capabilities.SupportsPersistentIdentifier, + DeviceProfile = capabilities.DeviceProfile, + AppStoreUrl = capabilities.AppStoreUrl, + IconUrl = capabilities.IconUrl + }; + } } } diff --git a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs index 635e4eb3d7..daf4d96313 100644 --- a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs +++ b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs @@ -1,20 +1,31 @@ #nullable disable -#pragma warning disable CS1591 - -using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; -namespace MediaBrowser.Controller.Authentication +namespace MediaBrowser.Controller.Authentication; + +/// +/// A class representing an authentication result. +/// +public class AuthenticationResult { - public class AuthenticationResult - { - public UserDto User { get; set; } + /// + /// Gets or sets the user. + /// + public UserDto User { get; set; } - public SessionInfo SessionInfo { get; set; } + /// + /// Gets or sets the session info. + /// + public SessionInfoDto SessionInfo { get; set; } - public string AccessToken { get; set; } + /// + /// Gets or sets the access token. + /// + public string AccessToken { get; set; } - public string ServerId { get; set; } - } + /// + /// Gets or sets the server id. + /// + public string ServerId { get; set; } } diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index 5566421cbe..cade53d994 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -1,81 +1,117 @@ -#nullable disable - -#pragma warning disable CS1591 - using System; using System.Threading.Tasks; +using Jellyfin.Data.Dtos; using Jellyfin.Data.Entities; using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Events; using Jellyfin.Data.Queries; using MediaBrowser.Model.Devices; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; -namespace MediaBrowser.Controller.Devices +namespace MediaBrowser.Controller.Devices; + +/// +/// Device manager interface. +/// +public interface IDeviceManager { - public interface IDeviceManager - { - event EventHandler>> DeviceOptionsUpdated; + /// + /// Event handler for updated device options. + /// + event EventHandler>> DeviceOptionsUpdated; + + /// + /// Creates a new device. + /// + /// The device to create. + /// A representing the creation of the device. + Task CreateDevice(Device device); - /// - /// Creates a new device. - /// - /// The device to create. - /// A representing the creation of the device. - Task CreateDevice(Device device); + /// + /// Saves the capabilities. + /// + /// The device id. + /// The capabilities. + void SaveCapabilities(string deviceId, ClientCapabilities capabilities); - /// - /// Saves the capabilities. - /// - /// The device id. - /// The capabilities. - void SaveCapabilities(string deviceId, ClientCapabilities capabilities); + /// + /// Gets the capabilities. + /// + /// The device id. + /// ClientCapabilities. + ClientCapabilities GetCapabilities(string? deviceId); - /// - /// Gets the capabilities. - /// - /// The device id. - /// ClientCapabilities. - ClientCapabilities GetCapabilities(string deviceId); + /// + /// Gets the device information. + /// + /// The identifier. + /// DeviceInfoDto. + DeviceInfoDto? GetDevice(string id); - /// - /// Gets the device information. - /// - /// The identifier. - /// DeviceInfo. - DeviceInfo GetDevice(string id); + /// + /// Gets devices based on the provided query. + /// + /// The device query. + /// A representing the retrieval of the devices. + QueryResult GetDevices(DeviceQuery query); - /// - /// Gets devices based on the provided query. - /// - /// The device query. - /// A representing the retrieval of the devices. - QueryResult GetDevices(DeviceQuery query); + /// + /// Gets device infromation based on the provided query. + /// + /// The device query. + /// A representing the retrieval of the device information. + QueryResult GetDeviceInfos(DeviceQuery query); - QueryResult GetDeviceInfos(DeviceQuery query); + /// + /// Gets the device information. + /// + /// The user's id, or null. + /// IEnumerable<DeviceInfoDto>. + QueryResult GetDevicesForUser(Guid? userId); - /// - /// Gets the devices. - /// - /// The user's id, or null. - /// IEnumerable<DeviceInfo>. - QueryResult GetDevicesForUser(Guid? userId); + /// + /// Deletes a device. + /// + /// The device. + /// A representing the deletion of the device. + Task DeleteDevice(Device device); - Task DeleteDevice(Device device); + /// + /// Updates a device. + /// + /// The device. + /// A representing the update of the device. + Task UpdateDevice(Device device); - Task UpdateDevice(Device device); + /// + /// Determines whether this instance [can access device] the specified user identifier. + /// + /// The user to test. + /// The device id to test. + /// Whether the user can access the device. + bool CanAccessDevice(User user, string deviceId); - /// - /// Determines whether this instance [can access device] the specified user identifier. - /// - /// The user to test. - /// The device id to test. - /// Whether the user can access the device. - bool CanAccessDevice(User user, string deviceId); + /// + /// Updates the options of a device. + /// + /// The device id. + /// The device name. + /// A representing the update of the device options. + Task UpdateDeviceOptions(string deviceId, string? deviceName); - Task UpdateDeviceOptions(string deviceId, string deviceName); + /// + /// Gets the options of a device. + /// + /// The device id. + /// of the device. + DeviceOptionsDto? GetDeviceOptions(string deviceId); - DeviceOptions GetDeviceOptions(string deviceId); - } + /// + /// Gets the dto for client capabilites. + /// + /// The client capabilities. + /// of the device. + ClientCapabilitiesDto ToClientCapabilitiesDto(ClientCapabilities capabilities); } diff --git a/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs b/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs index 357ef9406d..1542c58b35 100644 --- a/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs +++ b/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs @@ -1,6 +1,5 @@ using System; using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; namespace MediaBrowser.Controller.Events.Authentication; @@ -29,7 +28,7 @@ public class AuthenticationResultEventArgs : EventArgs /// /// Gets or sets the session information. /// - public SessionInfo? SessionInfo { get; set; } + public SessionInfoDto? SessionInfo { get; set; } /// /// Gets or sets the server id. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs index 3504831b87..8330745418 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.ComponentModel; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Session; namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; @@ -8,13 +9,13 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// /// Sessions message. /// -public class SessionsMessage : OutboundWebSocketMessage> +public class SessionsMessage : OutboundWebSocketMessage> { /// /// Initializes a new instance of the class. /// /// Session info. - public SessionsMessage(IReadOnlyList data) + public SessionsMessage(IReadOnlyList data) : base(data) { } diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 5a47236f92..f2e98dd787 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Jellyfin.Data.Entities.Security; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Session; using MediaBrowser.Model.SyncPlay; @@ -292,6 +293,16 @@ namespace MediaBrowser.Controller.Session /// SessionInfo. SessionInfo GetSession(string deviceId, string client, string version); + /// + /// Gets all sessions available to a user. + /// + /// The session identifier. + /// The device id. + /// Active within session limit. + /// Filter for sessions remote controllable for this user. + /// IReadOnlyList{SessionInfoDto}. + IReadOnlyList GetSessions(Guid userId, string deviceId, int? activeWithinSeconds, Guid? controllableUserToCheck); + /// /// Gets the session by authentication token. /// diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index 9e33588187..3ba1bfce42 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Linq; @@ -27,28 +25,45 @@ namespace MediaBrowser.Controller.Session private readonly ISessionManager _sessionManager; private readonly ILogger _logger; - private readonly object _progressLock = new object(); + private readonly object _progressLock = new(); private Timer _progressTimer; private PlaybackProgressInfo _lastProgressInfo; - private bool _disposed = false; + private bool _disposed; + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + /// Instance of interface. public SessionInfo(ISessionManager sessionManager, ILogger logger) { _sessionManager = sessionManager; _logger = logger; - AdditionalUsers = Array.Empty(); + AdditionalUsers = []; PlayState = new PlayerStateInfo(); - SessionControllers = Array.Empty(); - NowPlayingQueue = Array.Empty(); - NowPlayingQueueFullItems = Array.Empty(); + SessionControllers = []; + NowPlayingQueue = []; + NowPlayingQueueFullItems = []; } + /// + /// Gets or sets the play state. + /// + /// The play state. public PlayerStateInfo PlayState { get; set; } - public SessionUserInfo[] AdditionalUsers { get; set; } + /// + /// Gets or sets the additional users. + /// + /// The additional users. + public IReadOnlyList AdditionalUsers { get; set; } + /// + /// Gets or sets the client capabilities. + /// + /// The client capabilities. public ClientCapabilities Capabilities { get; set; } /// @@ -67,7 +82,7 @@ namespace MediaBrowser.Controller.Session { if (Capabilities is null) { - return Array.Empty(); + return []; } return Capabilities.PlayableMediaTypes; @@ -134,9 +149,17 @@ namespace MediaBrowser.Controller.Session /// The now playing item. public BaseItemDto NowPlayingItem { get; set; } + /// + /// Gets or sets the now playing queue full items. + /// + /// The now playing queue full items. [JsonIgnore] public BaseItem FullNowPlayingItem { get; set; } + /// + /// Gets or sets the now viewing item. + /// + /// The now viewing item. public BaseItemDto NowViewingItem { get; set; } /// @@ -156,8 +179,12 @@ namespace MediaBrowser.Controller.Session /// /// The session controller. [JsonIgnore] - public ISessionController[] SessionControllers { get; set; } + public IReadOnlyList SessionControllers { get; set; } + /// + /// Gets or sets the transcoding info. + /// + /// The transcoding info. public TranscodingInfo TranscodingInfo { get; set; } /// @@ -177,7 +204,7 @@ namespace MediaBrowser.Controller.Session } } - if (controllers.Length > 0) + if (controllers.Count > 0) { return false; } @@ -186,6 +213,10 @@ namespace MediaBrowser.Controller.Session } } + /// + /// Gets a value indicating whether the session supports media control. + /// + /// true if this session supports media control; otherwise, false. public bool SupportsMediaControl { get @@ -208,6 +239,10 @@ namespace MediaBrowser.Controller.Session } } + /// + /// Gets a value indicating whether the session supports remote control. + /// + /// true if this session supports remote control; otherwise, false. public bool SupportsRemoteControl { get @@ -230,16 +265,40 @@ namespace MediaBrowser.Controller.Session } } + /// + /// Gets or sets the now playing queue. + /// + /// The now playing queue. public IReadOnlyList NowPlayingQueue { get; set; } + /// + /// Gets or sets the now playing queue full items. + /// + /// The now playing queue full items. public IReadOnlyList NowPlayingQueueFullItems { get; set; } + /// + /// Gets or sets a value indicating whether the session has a custom device name. + /// + /// true if this session has a custom device name; otherwise, false. public bool HasCustomDeviceName { get; set; } + /// + /// Gets or sets the playlist item id. + /// + /// The splaylist item id. public string PlaylistItemId { get; set; } + /// + /// Gets or sets the server id. + /// + /// The server id. public string ServerId { get; set; } + /// + /// Gets or sets the user primary image tag. + /// + /// The user primary image tag. public string UserPrimaryImageTag { get; set; } /// @@ -247,8 +306,14 @@ namespace MediaBrowser.Controller.Session /// /// The supported commands. public IReadOnlyList SupportedCommands - => Capabilities is null ? Array.Empty() : Capabilities.SupportedCommands; + => Capabilities is null ? [] : Capabilities.SupportedCommands; + /// + /// Ensures a controller of type exists. + /// + /// Class to register. + /// The factory. + /// Tuple{ISessionController, bool}. public Tuple EnsureController(Func factory) { var controllers = SessionControllers.ToList(); @@ -261,18 +326,27 @@ namespace MediaBrowser.Controller.Session } var newController = factory(this); - _logger.LogDebug("Creating new {0}", newController.GetType().Name); + _logger.LogDebug("Creating new {Factory}", newController.GetType().Name); controllers.Add(newController); - SessionControllers = controllers.ToArray(); + SessionControllers = [.. controllers]; return new Tuple(newController, true); } + /// + /// Adds a controller to the session. + /// + /// The controller. public void AddController(ISessionController controller) { - SessionControllers = [..SessionControllers, controller]; + SessionControllers = [.. SessionControllers, controller]; } + /// + /// Gets a value indicating whether the session contains a user. + /// + /// The user id to check. + /// true if this session contains the user; otherwise, false. public bool ContainsUser(Guid userId) { if (UserId.Equals(userId)) @@ -291,6 +365,11 @@ namespace MediaBrowser.Controller.Session return false; } + /// + /// Starts automatic progressing. + /// + /// The playback progress info. + /// The supported commands. public void StartAutomaticProgress(PlaybackProgressInfo progressInfo) { if (_disposed) @@ -359,6 +438,9 @@ namespace MediaBrowser.Controller.Session } } + /// + /// Stops automatic progressing. + /// public void StopAutomaticProgress() { lock (_progressLock) @@ -373,6 +455,10 @@ namespace MediaBrowser.Controller.Session } } + /// + /// Disposes the instance async. + /// + /// ValueTask. public async ValueTask DisposeAsync() { _disposed = true; @@ -380,7 +466,7 @@ namespace MediaBrowser.Controller.Session StopAutomaticProgress(); var controllers = SessionControllers.ToList(); - SessionControllers = Array.Empty(); + SessionControllers = []; foreach (var controller in controllers) { diff --git a/MediaBrowser.Model/Devices/DeviceInfo.cs b/MediaBrowser.Model/Devices/DeviceInfo.cs index 4962992a0a..1155986138 100644 --- a/MediaBrowser.Model/Devices/DeviceInfo.cs +++ b/MediaBrowser.Model/Devices/DeviceInfo.cs @@ -1,69 +1,84 @@ -#nullable disable -#pragma warning disable CS1591 - using System; using MediaBrowser.Model.Session; -namespace MediaBrowser.Model.Devices +namespace MediaBrowser.Model.Devices; + +/// +/// A class for device Information. +/// +public class DeviceInfo { - public class DeviceInfo + /// + /// Initializes a new instance of the class. + /// + public DeviceInfo() { - public DeviceInfo() - { - Capabilities = new ClientCapabilities(); - } + Capabilities = new ClientCapabilities(); + } - public string Name { get; set; } + /// + /// Gets or sets the name. + /// + /// The name. + public string? Name { get; set; } - public string CustomName { get; set; } + /// + /// Gets or sets the custom name. + /// + /// The custom name. + public string? CustomName { get; set; } - /// - /// Gets or sets the access token. - /// - public string AccessToken { get; set; } + /// + /// Gets or sets the access token. + /// + /// The access token. + public string? AccessToken { get; set; } - /// - /// Gets or sets the identifier. - /// - /// The identifier. - public string Id { get; set; } + /// + /// Gets or sets the identifier. + /// + /// The identifier. + public string? Id { get; set; } - /// - /// Gets or sets the last name of the user. - /// - /// The last name of the user. - public string LastUserName { get; set; } + /// + /// Gets or sets the last name of the user. + /// + /// The last name of the user. + public string? LastUserName { get; set; } - /// - /// Gets or sets the name of the application. - /// - /// The name of the application. - public string AppName { get; set; } + /// + /// Gets or sets the name of the application. + /// + /// The name of the application. + public string? AppName { get; set; } - /// - /// Gets or sets the application version. - /// - /// The application version. - public string AppVersion { get; set; } + /// + /// Gets or sets the application version. + /// + /// The application version. + public string? AppVersion { get; set; } - /// - /// Gets or sets the last user identifier. - /// - /// The last user identifier. - public Guid LastUserId { get; set; } + /// + /// Gets or sets the last user identifier. + /// + /// The last user identifier. + public Guid? LastUserId { get; set; } - /// - /// Gets or sets the date last modified. - /// - /// The date last modified. - public DateTime DateLastActivity { get; set; } + /// + /// Gets or sets the date last modified. + /// + /// The date last modified. + public DateTime? DateLastActivity { get; set; } - /// - /// Gets or sets the capabilities. - /// - /// The capabilities. - public ClientCapabilities Capabilities { get; set; } + /// + /// Gets or sets the capabilities. + /// + /// The capabilities. + public ClientCapabilities Capabilities { get; set; } - public string IconUrl { get; set; } - } + /// + /// Gets or sets the icon URL. + /// + /// The icon URL. + public string? IconUrl { get; set; } } diff --git a/MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs b/MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs new file mode 100644 index 0000000000..5963ed270d --- /dev/null +++ b/MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; +using Jellyfin.Extensions.Json.Converters; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Model.Dto; + +/// +/// Client capabilities dto. +/// +public class ClientCapabilitiesDto +{ + /// + /// Gets or sets the list of playable media types. + /// + [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + public IReadOnlyList PlayableMediaTypes { get; set; } = []; + + /// + /// Gets or sets the list of supported commands. + /// + [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + public IReadOnlyList SupportedCommands { get; set; } = []; + + /// + /// Gets or sets a value indicating whether session supports media control. + /// + public bool SupportsMediaControl { get; set; } + + /// + /// Gets or sets a value indicating whether session supports a persistent identifier. + /// + public bool SupportsPersistentIdentifier { get; set; } + + /// + /// Gets or sets the device profile. + /// + public DeviceProfile? DeviceProfile { get; set; } + + /// + /// Gets or sets the app store url. + /// + public string? AppStoreUrl { get; set; } + + /// + /// Gets or sets the icon url. + /// + public string? IconUrl { get; set; } + + /// + /// Convert the dto to the full model. + /// + /// The converted model. + public ClientCapabilities ToClientCapabilities() + { + return new ClientCapabilities + { + PlayableMediaTypes = PlayableMediaTypes, + SupportedCommands = SupportedCommands, + SupportsMediaControl = SupportsMediaControl, + SupportsPersistentIdentifier = SupportsPersistentIdentifier, + DeviceProfile = DeviceProfile, + AppStoreUrl = AppStoreUrl, + IconUrl = IconUrl + }; + } +} diff --git a/MediaBrowser.Model/Dto/DeviceInfoDto.cs b/MediaBrowser.Model/Dto/DeviceInfoDto.cs new file mode 100644 index 0000000000..ac7a731a90 --- /dev/null +++ b/MediaBrowser.Model/Dto/DeviceInfoDto.cs @@ -0,0 +1,83 @@ +using System; + +namespace MediaBrowser.Model.Dto; + +/// +/// A DTO representing device information. +/// +public class DeviceInfoDto +{ + /// + /// Initializes a new instance of the class. + /// + public DeviceInfoDto() + { + Capabilities = new ClientCapabilitiesDto(); + } + + /// + /// Gets or sets the name. + /// + /// The name. + public string? Name { get; set; } + + /// + /// Gets or sets the custom name. + /// + /// The custom name. + public string? CustomName { get; set; } + + /// + /// Gets or sets the access token. + /// + /// The access token. + public string? AccessToken { get; set; } + + /// + /// Gets or sets the identifier. + /// + /// The identifier. + public string? Id { get; set; } + + /// + /// Gets or sets the last name of the user. + /// + /// The last name of the user. + public string? LastUserName { get; set; } + + /// + /// Gets or sets the name of the application. + /// + /// The name of the application. + public string? AppName { get; set; } + + /// + /// Gets or sets the application version. + /// + /// The application version. + public string? AppVersion { get; set; } + + /// + /// Gets or sets the last user identifier. + /// + /// The last user identifier. + public Guid? LastUserId { get; set; } + + /// + /// Gets or sets the date last modified. + /// + /// The date last modified. + public DateTime? DateLastActivity { get; set; } + + /// + /// Gets or sets the capabilities. + /// + /// The capabilities. + public ClientCapabilitiesDto Capabilities { get; set; } + + /// + /// Gets or sets the icon URL. + /// + /// The icon URL. + public string? IconUrl { get; set; } +} diff --git a/MediaBrowser.Model/Dto/SessionInfoDto.cs b/MediaBrowser.Model/Dto/SessionInfoDto.cs new file mode 100644 index 0000000000..2496c933a2 --- /dev/null +++ b/MediaBrowser.Model/Dto/SessionInfoDto.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using Jellyfin.Data.Enums; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Model.Dto; + +/// +/// Session info DTO. +/// +public class SessionInfoDto +{ + /// + /// Gets or sets the play state. + /// + /// The play state. + public PlayerStateInfo? PlayState { get; set; } + + /// + /// Gets or sets the additional users. + /// + /// The additional users. + public IReadOnlyList? AdditionalUsers { get; set; } + + /// + /// Gets or sets the client capabilities. + /// + /// The client capabilities. + public ClientCapabilitiesDto? Capabilities { get; set; } + + /// + /// Gets or sets the remote end point. + /// + /// The remote end point. + public string? RemoteEndPoint { get; set; } + + /// + /// Gets or sets the playable media types. + /// + /// The playable media types. + public IReadOnlyList PlayableMediaTypes { get; set; } = []; + + /// + /// Gets or sets the id. + /// + /// The id. + public string? Id { get; set; } + + /// + /// Gets or sets the user id. + /// + /// The user id. + public Guid UserId { get; set; } + + /// + /// Gets or sets the username. + /// + /// The username. + public string? UserName { get; set; } + + /// + /// Gets or sets the type of the client. + /// + /// The type of the client. + public string? Client { get; set; } + + /// + /// Gets or sets the last activity date. + /// + /// The last activity date. + public DateTime LastActivityDate { get; set; } + + /// + /// Gets or sets the last playback check in. + /// + /// The last playback check in. + public DateTime LastPlaybackCheckIn { get; set; } + + /// + /// Gets or sets the last paused date. + /// + /// The last paused date. + public DateTime? LastPausedDate { get; set; } + + /// + /// Gets or sets the name of the device. + /// + /// The name of the device. + public string? DeviceName { get; set; } + + /// + /// Gets or sets the type of the device. + /// + /// The type of the device. + public string? DeviceType { get; set; } + + /// + /// Gets or sets the now playing item. + /// + /// The now playing item. + public BaseItemDto? NowPlayingItem { get; set; } + + /// + /// Gets or sets the now viewing item. + /// + /// The now viewing item. + public BaseItemDto? NowViewingItem { get; set; } + + /// + /// Gets or sets the device id. + /// + /// The device id. + public string? DeviceId { get; set; } + + /// + /// Gets or sets the application version. + /// + /// The application version. + public string? ApplicationVersion { get; set; } + + /// + /// Gets or sets the transcoding info. + /// + /// The transcoding info. + public TranscodingInfo? TranscodingInfo { get; set; } + + /// + /// Gets or sets a value indicating whether this session is active. + /// + /// true if this session is active; otherwise, false. + public bool IsActive { get; set; } + + /// + /// Gets or sets a value indicating whether the session supports media control. + /// + /// true if this session supports media control; otherwise, false. + public bool SupportsMediaControl { get; set; } + + /// + /// Gets or sets a value indicating whether the session supports remote control. + /// + /// true if this session supports remote control; otherwise, false. + public bool SupportsRemoteControl { get; set; } + + /// + /// Gets or sets the now playing queue. + /// + /// The now playing queue. + public IReadOnlyList? NowPlayingQueue { get; set; } + + /// + /// Gets or sets the now playing queue full items. + /// + /// The now playing queue full items. + public IReadOnlyList? NowPlayingQueueFullItems { get; set; } + + /// + /// Gets or sets a value indicating whether the session has a custom device name. + /// + /// true if this session has a custom device name; otherwise, false. + public bool HasCustomDeviceName { get; set; } + + /// + /// Gets or sets the playlist item id. + /// + /// The splaylist item id. + public string? PlaylistItemId { get; set; } + + /// + /// Gets or sets the server id. + /// + /// The server id. + public string? ServerId { get; set; } + + /// + /// Gets or sets the user primary image tag. + /// + /// The user primary image tag. + public string? UserPrimaryImageTag { get; set; } + + /// + /// Gets or sets the supported commands. + /// + /// The supported commands. + public IReadOnlyList SupportedCommands { get; set; } = []; +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs index 1466d3a71a..b9477ce6b7 100644 --- a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs +++ b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; @@ -35,38 +37,27 @@ namespace Jellyfin.Extensions.Json.Converters var stringEntries = reader.GetString()!.Split(Delimiter, StringSplitOptions.RemoveEmptyEntries); if (stringEntries.Length == 0) { - return Array.Empty(); + return []; } - var parsedValues = new object[stringEntries.Length]; - var convertedCount = 0; + var typedValues = new List(); for (var i = 0; i < stringEntries.Length; i++) { try { - parsedValues[i] = _typeConverter.ConvertFromInvariantString(stringEntries[i].Trim()) ?? throw new FormatException(); - convertedCount++; + var parsedValue = _typeConverter.ConvertFromInvariantString(stringEntries[i].Trim()); + if (parsedValue is not null) + { + typedValues.Add((T)parsedValue); + } } catch (FormatException) { - // TODO log when upgraded to .Net6 - // https://github.com/dotnet/runtime/issues/42975 - // _logger.LogDebug(e, "Error converting value."); + // Ignore unconvertable inputs } } - var typedValues = new T[convertedCount]; - var typedValueIndex = 0; - for (var i = 0; i < stringEntries.Length; i++) - { - if (parsedValues[i] is not null) - { - typedValues.SetValue(parsedValues[i], typedValueIndex); - typedValueIndex++; - } - } - - return typedValues; + return [.. typedValues]; } return JsonSerializer.Deserialize(ref reader, options); @@ -75,7 +66,39 @@ namespace Jellyfin.Extensions.Json.Converters /// public override void Write(Utf8JsonWriter writer, T[]? value, JsonSerializerOptions options) { - throw new NotImplementedException(); + if (value is not null) + { + writer.WriteStartArray(); + if (value.Length > 0) + { + var toWrite = value.Length - 1; + foreach (var it in value) + { + var wrote = false; + if (it is not null) + { + writer.WriteStringValue(it.ToString()); + wrote = true; + } + + if (toWrite > 0) + { + if (wrote) + { + writer.WriteStringValue(Delimiter.ToString()); + } + + toWrite--; + } + } + } + + writer.WriteEndArray(); + } + else + { + writer.WriteNullValue(); + } } } } diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs index 61105b42b2..9fc0158235 100644 --- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs +++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs @@ -41,7 +41,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { "a", "b", "c" } + Value = ["a", "b", "c"] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a,b,c"" }", _jsonOptions); @@ -53,7 +53,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { "a", "b", "c" } + Value = ["a", "b", "c"] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a, b, c"" }", _jsonOptions); @@ -65,7 +65,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,MoveDown"" }", _jsonOptions); @@ -77,7 +77,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,,MoveDown"" }", _jsonOptions); @@ -89,7 +89,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,TotallyNotAVallidCommand,MoveDown"" }", _jsonOptions); @@ -101,7 +101,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp, MoveDown"" }", _jsonOptions); @@ -113,7 +113,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { "a", "b", "c" } + Value = ["a", "b", "c"] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""a"",""b"",""c""] }", _jsonOptions); @@ -125,7 +125,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", _jsonOptions); -- cgit v1.2.3 From 3c639c2e80f2a17eea3f5f1a70c1b287bc99aba4 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Mon, 23 Sep 2024 09:09:23 -0600 Subject: Tweak Trickplay migration for speed (#12643) --- .../Trickplay/TrickplayManager.cs | 8 +-- .../Migrations/Routines/MoveTrickplayFiles.cs | 57 +++++++++++++++++----- .../Trickplay/ITrickplayManager.cs | 4 +- .../Trickplay/TrickplayMoveImagesTask.cs | 51 +++++++++++-------- 4 files changed, 82 insertions(+), 38 deletions(-) (limited to 'Jellyfin.Server.Implementations') diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index 861037c1fe..73e31279f4 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -455,16 +455,18 @@ public class TrickplayManager : ITrickplayManager } /// - public async Task> GetTrickplayItemsAsync() + public async Task> GetTrickplayItemsAsync(int limit, int offset) { - List trickplayItems; + IReadOnlyList trickplayItems; var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { trickplayItems = await dbContext.TrickplayInfos .AsNoTracking() - .Select(i => i.ItemId) + .OrderBy(i => i.ItemId) + .Skip(offset) + .Take(limit) .ToListAsync() .ConfigureAwait(false); } diff --git a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs index 301541b6ce..c1a9e88949 100644 --- a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs +++ b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs @@ -1,10 +1,15 @@ using System; +using System.Diagnostics; using System.Globalization; using System.IO; +using System.Linq; +using Jellyfin.Data.Enums; +using MediaBrowser.Common; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Migrations.Routines; @@ -16,6 +21,7 @@ public class MoveTrickplayFiles : IMigrationRoutine private readonly ITrickplayManager _trickplayManager; private readonly IFileSystem _fileSystem; private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. @@ -23,11 +29,13 @@ public class MoveTrickplayFiles : IMigrationRoutine /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - public MoveTrickplayFiles(ITrickplayManager trickplayManager, IFileSystem fileSystem, ILibraryManager libraryManager) + /// The logger. + public MoveTrickplayFiles(ITrickplayManager trickplayManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILogger logger) { _trickplayManager = trickplayManager; _fileSystem = fileSystem; _libraryManager = libraryManager; + _logger = logger; } /// @@ -42,26 +50,49 @@ public class MoveTrickplayFiles : IMigrationRoutine /// public void Perform() { - var trickplayItems = _trickplayManager.GetTrickplayItemsAsync().GetAwaiter().GetResult(); - foreach (var itemId in trickplayItems) + const int Limit = 100; + int itemCount = 0, offset = 0, previousCount; + + var sw = Stopwatch.StartNew(); + var trickplayQuery = new InternalItemsQuery { - var resolutions = _trickplayManager.GetTrickplayResolutions(itemId).GetAwaiter().GetResult(); - var item = _libraryManager.GetItemById(itemId); - if (item is null) - { - continue; - } + MediaTypes = [MediaType.Video], + SourceTypes = [SourceType.Library], + IsVirtualItem = false, + IsFolder = false + }; - foreach (var resolution in resolutions) + do + { + var trickplayInfos = _trickplayManager.GetTrickplayItemsAsync(Limit, offset).GetAwaiter().GetResult(); + previousCount = trickplayInfos.Count; + offset += Limit; + + trickplayQuery.ItemIds = trickplayInfos.Select(i => i.ItemId).Distinct().ToArray(); + var items = _libraryManager.GetItemList(trickplayQuery); + foreach (var trickplayInfo in trickplayInfos) { - var oldPath = GetOldTrickplayDirectory(item, resolution.Key); - var newPath = _trickplayManager.GetTrickplayDirectory(item, resolution.Value.TileWidth, resolution.Value.TileHeight, resolution.Value.Width, false); + var item = items.OfType