diff options
31 files changed, 284 insertions, 229 deletions
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 4c5dcdafc..2bf8eacb1 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -38,7 +38,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0" PrivateAssets="All" /> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" /> </ItemGroup> <!-- Code Analyzers--> diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs index 44dad5b17..11e33278d 100644 --- a/Emby.Server.Implementations/Data/ManagedConnection.cs +++ b/Emby.Server.Implementations/Data/ManagedConnection.cs @@ -7,7 +7,7 @@ using SQLitePCL.pretty; namespace Emby.Server.Implementations.Data { - public class ManagedConnection : IDisposable + public sealed class ManagedConnection : IDisposable { private readonly SemaphoreSlim _writeLock; diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index 47a83d77c..e62361c1e 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -14,7 +12,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.IO { - public class FileRefresher : IDisposable + public sealed class FileRefresher : IDisposable { private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; @@ -22,7 +20,7 @@ namespace Emby.Server.Implementations.IO private readonly List<string> _affectedPaths = new List<string>(); private readonly object _timerLock = new object(); - private Timer _timer; + private Timer? _timer; private bool _disposed; public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger) @@ -36,7 +34,7 @@ namespace Emby.Server.Implementations.IO AddPath(path); } - public event EventHandler<EventArgs> Completed; + public event EventHandler<EventArgs>? Completed; public string Path { get; private set; } @@ -111,7 +109,7 @@ namespace Emby.Server.Implementations.IO RestartTimer(); } - private void OnTimerCallback(object state) + private void OnTimerCallback(object? state) { List<string> paths; @@ -127,7 +125,7 @@ namespace Emby.Server.Implementations.IO try { - ProcessPathChanges(paths.ToList()); + ProcessPathChanges(paths); } catch (Exception ex) { @@ -137,12 +135,12 @@ namespace Emby.Server.Implementations.IO private void ProcessPathChanges(List<string> paths) { - var itemsToRefresh = paths + IEnumerable<BaseItem> itemsToRefresh = paths .Distinct(StringComparer.OrdinalIgnoreCase) .Select(GetAffectedBaseItem) .Where(item => item != null) - .GroupBy(x => x.Id) - .Select(x => x.First()); + .GroupBy(x => x!.Id) // Removed null values in the previous .Where() + .Select(x => x.First())!; foreach (var item in itemsToRefresh) { @@ -176,15 +174,15 @@ namespace Emby.Server.Implementations.IO /// </summary> /// <param name="path">The path.</param> /// <returns>BaseItem.</returns> - private BaseItem GetAffectedBaseItem(string path) + private BaseItem? GetAffectedBaseItem(string path) { - BaseItem item = null; + BaseItem? item = null; while (item == null && !string.IsNullOrEmpty(path)) { item = _libraryManager.FindByPath(path, null); - path = System.IO.Path.GetDirectoryName(path); + path = System.IO.Path.GetDirectoryName(path) ?? string.Empty; } if (item != null) diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index b525f5a2f..9fcc7fe59 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -449,12 +449,12 @@ namespace Emby.Server.Implementations.IO } var newRefresher = new FileRefresher(path, _configurationManager, _libraryManager, _logger); - newRefresher.Completed += NewRefresher_Completed; + newRefresher.Completed += OnNewRefresherCompleted; _activeRefreshers.Add(newRefresher); } } - private void NewRefresher_Completed(object sender, EventArgs e) + private void OnNewRefresherCompleted(object sender, EventArgs e) { var refresher = (FileRefresher)sender; DisposeRefresher(refresher); @@ -481,6 +481,7 @@ namespace Emby.Server.Implementations.IO { lock (_activeRefreshers) { + refresher.Completed -= OnNewRefresherCompleted; refresher.Dispose(); _activeRefreshers.Remove(refresher); } @@ -492,6 +493,7 @@ namespace Emby.Server.Implementations.IO { foreach (var refresher in _activeRefreshers.ToList()) { + refresher.Completed -= OnNewRefresherCompleted; refresher.Dispose(); } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 5726d7158..a88a1fe84 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -187,7 +187,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV CultureInfo.InvariantCulture, "-i \"{0}\" {2} -map_metadata -1 -threads {6} {3}{4}{5} -y \"{1}\"", inputTempFile, - targetFile.Replace("\"", "\\\""), // Escape quotes in filename + targetFile.Replace("\"", "\\\"", StringComparison.Ordinal), // Escape quotes in filename videoArgs, GetAudioArgs(mediaSource), subtitleArgs, diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index f9d151a81..9ab4cc628 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken) { using var client = new TcpClient(); - client.Connect(remoteIp, HdHomeRunPort); + await client.ConnectAsync(remoteIp, HdHomeRunPort).ConfigureAwait(false); using var stream = client.GetStream(); return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index 506ef5548..708ff52d7 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -283,7 +283,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts // #EXTINF:0,84.0 - VOX Schweiz if (!string.IsNullOrWhiteSpace(nameInExtInf)) { - var numberIndex = nameInExtInf.IndexOf(' '); + var numberIndex = nameInExtInf.IndexOf(' ', StringComparison.Ordinal); if (numberIndex > 0) { var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' }); diff --git a/Emby.Server.Implementations/Localization/Core/as.json b/Emby.Server.Implementations/Localization/Core/as.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/as.json @@ -0,0 +1 @@ +{} diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json index e5405e515..626d76d6b 100644 --- a/Emby.Server.Implementations/Localization/Core/et.json +++ b/Emby.Server.Implementations/Localization/Core/et.json @@ -114,5 +114,7 @@ "Artists": "Esitajad", "Application": "Rakendus", "AppDeviceValues": "Rakendus: {0}, seade: {1}", - "Albums": "Albumid" + "Albums": "Albumid", + "UserOfflineFromDevice": "{0} katkestas ühenduse {1}-ga", + "SubtitleDownloadFailureFromForItem": "Subtiitrite allalaadimine {0} > {1} nurjus" } diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index b07798fa4..9481e26f7 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -527,7 +527,7 @@ namespace Emby.Server.Implementations.Playlists var relativeUri = folderUri.MakeRelativeUri(fileAbsoluteUri); string relativePath = Uri.UnescapeDataString(relativeUri.ToString()); - if (fileAbsoluteUri.Scheme.Equals("file", StringComparison.CurrentCultureIgnoreCase)) + if (fileAbsoluteUri.Scheme.Equals("file", StringComparison.OrdinalIgnoreCase)) { relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); } diff --git a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs index bd1966623..67a9fbd3b 100644 --- a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs +++ b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs @@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.Sorting /// <returns>System.Int32.</returns> public int Compare(BaseItem? x, BaseItem? y) { - return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase); + return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase); } /// <summary> diff --git a/Emby.Server.Implementations/Sorting/AlbumComparer.cs b/Emby.Server.Implementations/Sorting/AlbumComparer.cs index fe7dc84cb..4d09dda84 100644 --- a/Emby.Server.Implementations/Sorting/AlbumComparer.cs +++ b/Emby.Server.Implementations/Sorting/AlbumComparer.cs @@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Sorting /// <returns>System.Int32.</returns> public int Compare(BaseItem? x, BaseItem? y) { - return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase); + return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase); } /// <summary> @@ -35,9 +35,7 @@ namespace Emby.Server.Implementations.Sorting /// <returns>System.String.</returns> private static string? GetValue(BaseItem? x) { - var audio = x as Audio; - - return audio == null ? string.Empty : audio.Album; + return x is Audio audio ? audio.Album : string.Empty; } } } diff --git a/Emby.Server.Implementations/Sorting/ArtistComparer.cs b/Emby.Server.Implementations/Sorting/ArtistComparer.cs index 7b7ba5753..a8bb55e2b 100644 --- a/Emby.Server.Implementations/Sorting/ArtistComparer.cs +++ b/Emby.Server.Implementations/Sorting/ArtistComparer.cs @@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.Sorting /// <inheritdoc /> public int Compare(BaseItem? x, BaseItem? y) { - return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase); + return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase); } /// <summary> diff --git a/Emby.Server.Implementations/Sorting/NameComparer.cs b/Emby.Server.Implementations/Sorting/NameComparer.cs index 8f87717f4..c2875eeb9 100644 --- a/Emby.Server.Implementations/Sorting/NameComparer.cs +++ b/Emby.Server.Implementations/Sorting/NameComparer.cs @@ -34,7 +34,7 @@ namespace Emby.Server.Implementations.Sorting throw new ArgumentNullException(nameof(y)); } - return string.Compare(x.Name, y.Name, StringComparison.CurrentCultureIgnoreCase); + return string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase); } } } diff --git a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs index 4123a59f8..0bd9600b9 100644 --- a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs +++ b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs @@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Sorting /// <returns>System.Int32.</returns> public int Compare(BaseItem x, BaseItem y) { - return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase); + return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase); } private static string GetValue(BaseItem item) diff --git a/Emby.Server.Implementations/Sorting/SortNameComparer.cs b/Emby.Server.Implementations/Sorting/SortNameComparer.cs index fb97a0349..79be9a89a 100644 --- a/Emby.Server.Implementations/Sorting/SortNameComparer.cs +++ b/Emby.Server.Implementations/Sorting/SortNameComparer.cs @@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Sorting throw new ArgumentNullException(nameof(y)); } - return string.Compare(x.SortName, y.SortName, StringComparison.CurrentCultureIgnoreCase); + return string.Compare(x.SortName, y.SortName, StringComparison.OrdinalIgnoreCase); } } } diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index e1cbc6f33..3c079a71d 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -451,7 +451,7 @@ namespace Jellyfin.Api.Controllers if (@static.HasValue && @static.Value && state.DirectStreamProvider != null) { - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager); + StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager); var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId); if (liveStreamInfo == null) @@ -467,7 +467,7 @@ namespace Jellyfin.Api.Controllers // Static remote stream if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http) { - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager); + StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager); var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, HttpContext).ConfigureAwait(false); @@ -484,7 +484,7 @@ namespace Jellyfin.Api.Controllers var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive); var isTranscodeCached = outputPathExists && transcodingJob != null; - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, startTimeTicks, Request, _dlnaManager); + StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, state.Request.StartTimeTicks, Request, _dlnaManager); // Static stream if (@static.HasValue && @static.Value) diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 58dd945c6..87233d907 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -24,7 +24,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0" PrivateAssets="All" /> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" /> </ItemGroup> <!-- Code analysers--> diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 441f06f69..4ed44baef 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -21,7 +21,7 @@ <ItemGroup> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" /> - <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0" PrivateAssets="All" /> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs index abfdb41d8..ba2f419a2 100644 --- a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs +++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs @@ -60,7 +60,7 @@ namespace MediaBrowser.Controller.BaseItemManager return false; } - var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase)); + var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, baseItem.GetType().Name, StringComparison.OrdinalIgnoreCase)); return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); } @@ -91,7 +91,7 @@ namespace MediaBrowser.Controller.BaseItemManager return false; } - var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase)); + var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, baseItem.GetType().Name, StringComparison.OrdinalIgnoreCase)); return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); } diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 1996335fe..cf3b7bc7a 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -14,10 +14,10 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Diacritics" Version="3.3.4" /> + <PackageReference Include="Diacritics" Version="3.3.10" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" /> - <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0" PrivateAssets="All" /> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" /> <PackageReference Include="System.Threading.Tasks.Dataflow" Version="6.0.0" /> </ItemGroup> diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index 7d62fb6e1..1418e583e 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; @@ -95,10 +96,10 @@ namespace MediaBrowser.Controller.MediaEncoding /// <param name="mediaSource">Media source information.</param> /// <param name="imageStream">Media stream information.</param> /// <param name="imageStreamIndex">Index of the stream to extract from.</param> - /// <param name="outputExtension">The extension of the file to write, including the '.'.</param> + /// <param name="targetFormat">The format of the file to write.</param> /// <param name="cancellationToken">CancellationToken to use for operation.</param> /// <returns>Location of video image.</returns> - Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, string outputExtension, CancellationToken cancellationToken); + Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, ImageFormat? targetFormat, CancellationToken cancellationToken); /// <summary> /// Gets the media info. diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index a2bac7b49..1c97a1982 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -19,6 +19,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.MediaEncoding.Probing; using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; @@ -478,17 +479,17 @@ namespace MediaBrowser.MediaEncoding.Encoder Protocol = MediaProtocol.File }; - return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, ".jpg", cancellationToken); + return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, ImageFormat.Jpg, cancellationToken); } public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken) { - return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, ".jpg", cancellationToken); + return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, ImageFormat.Jpg, cancellationToken); } - public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, string outputExtension, CancellationToken cancellationToken) + public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, ImageFormat? targetFormat, CancellationToken cancellationToken) { - return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, outputExtension, cancellationToken); + return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, targetFormat, cancellationToken); } private async Task<string> ExtractImage( @@ -500,7 +501,7 @@ namespace MediaBrowser.MediaEncoding.Encoder bool isAudio, Video3DFormat? threedFormat, TimeSpan? offset, - string outputExtension, + ImageFormat? targetFormat, CancellationToken cancellationToken) { var inputArgument = GetInputArgument(inputFile, mediaSource); @@ -510,7 +511,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // The failure of HDR extraction usually occurs when using custom ffmpeg that does not contain the zscale filter. try { - return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, true, outputExtension, cancellationToken).ConfigureAwait(false); + return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, true, targetFormat, cancellationToken).ConfigureAwait(false); } catch (ArgumentException) { @@ -523,7 +524,7 @@ namespace MediaBrowser.MediaEncoding.Encoder try { - return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, true, outputExtension, cancellationToken).ConfigureAwait(false); + return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, true, targetFormat, cancellationToken).ConfigureAwait(false); } catch (ArgumentException) { @@ -536,7 +537,7 @@ namespace MediaBrowser.MediaEncoding.Encoder try { - return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, false, outputExtension, cancellationToken).ConfigureAwait(false); + return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, false, targetFormat, cancellationToken).ConfigureAwait(false); } catch (ArgumentException) { @@ -548,24 +549,25 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, false, outputExtension, cancellationToken).ConfigureAwait(false); + return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, false, targetFormat, cancellationToken).ConfigureAwait(false); } - private async Task<string> ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, bool allowTonemap, string outputExtension, CancellationToken cancellationToken) + private async Task<string> ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, bool allowTonemap, ImageFormat? targetFormat, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(inputPath)) { throw new ArgumentNullException(nameof(inputPath)); } - if (string.IsNullOrEmpty(outputExtension)) + var outputExtension = targetFormat switch { - outputExtension = ".jpg"; - } - else if (outputExtension[0] != '.') - { - outputExtension = "." + outputExtension; - } + ImageFormat.Bmp => ".bmp", + ImageFormat.Gif => ".gif", + ImageFormat.Jpg => ".jpg", + ImageFormat.Png => ".png", + ImageFormat.Webp => ".webp", + _ => ".jpg" + }; var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension); Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath)); diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 9279cb220..32ff1dee6 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -721,15 +721,13 @@ namespace MediaBrowser.MediaEncoding.Probing } else if (string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase)) { - stream.Type = isAudio || string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase) || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) || string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase) - ? MediaStreamType.EmbeddedImage - : MediaStreamType.Video; - stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate); stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate); - if (isAudio || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) || - string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)) + if (isAudio + || string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase) + || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) + || string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)) { stream.Type = MediaStreamType.EmbeddedImage; } diff --git a/MediaBrowser.Model/Dto/DisplayPreferencesDto.cs b/MediaBrowser.Model/Dto/DisplayPreferencesDto.cs index 6a4453536..90163ae91 100644 --- a/MediaBrowser.Model/Dto/DisplayPreferencesDto.cs +++ b/MediaBrowser.Model/Dto/DisplayPreferencesDto.cs @@ -63,10 +63,10 @@ namespace MediaBrowser.Model.Dto public int PrimaryImageWidth { get; set; } /// <summary> - /// Gets the custom prefs. + /// Gets or sets the custom prefs. /// </summary> /// <value>The custom prefs.</value> - public Dictionary<string, string?> CustomPrefs { get; } + public Dictionary<string, string?> CustomPrefs { get; set; } /// <summary> /// Gets or sets the scroll direction. diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 85947b3de..70fef5d66 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -29,7 +29,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0" PrivateAssets="All" /> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" /> <PackageReference Include="System.Globalization" Version="4.3.0" /> <PackageReference Include="System.Text.Json" Version="6.0.0" /> diff --git a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs index 9b63971a9..186e55f1d 100644 --- a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs @@ -15,6 +15,7 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Net; +using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.MediaInfo { @@ -45,14 +46,17 @@ namespace MediaBrowser.Providers.MediaInfo }; private readonly IMediaEncoder _mediaEncoder; + private readonly ILogger<EmbeddedImageProvider> _logger; /// <summary> /// Initializes a new instance of the <see cref="EmbeddedImageProvider"/> class. /// </summary> /// <param name="mediaEncoder">The media encoder for extracting attached/embedded images.</param> - public EmbeddedImageProvider(IMediaEncoder mediaEncoder) + /// <param name="logger">The logger.</param> + public EmbeddedImageProvider(IMediaEncoder mediaEncoder, ILogger<EmbeddedImageProvider> logger) { _mediaEncoder = mediaEncoder; + _logger = logger; } /// <inheritdoc /> @@ -114,9 +118,15 @@ namespace MediaBrowser.Providers.MediaInfo ImageType.Primary => _primaryImageFileNames, ImageType.Backdrop => _backdropImageFileNames, ImageType.Logo => _logoImageFileNames, - _ => _primaryImageFileNames + _ => Array.Empty<string>() }; + if (imageFileNames.Length == 0) + { + _logger.LogWarning("Attempted to load unexpected image type: {Type}", type); + return new DynamicImageResponse { HasImage = false }; + } + // Try attachments first var attachmentStream = item.GetMediaSources(false) .SelectMany(source => source.MediaAttachments) @@ -156,13 +166,21 @@ namespace MediaBrowser.Providers.MediaInfo } } + var format = imageStream.Codec switch + { + "mjpeg" => ImageFormat.Jpg, + "png" => ImageFormat.Png, + "gif" => ImageFormat.Gif, + _ => ImageFormat.Jpg + }; + string extractedImagePath = - await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, imageStream, imageStream.Index, ".jpg", cancellationToken) + await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, imageStream, imageStream.Index, format, cancellationToken) .ConfigureAwait(false); return new DynamicImageResponse { - Format = ImageFormat.Jpg, + Format = format, HasImage = true, Path = extractedImagePath, Protocol = MediaProtocol.File @@ -180,10 +198,6 @@ namespace MediaBrowser.Providers.MediaInfo extension = ".jpg"; } - string extractedAttachmentPath = - await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, null, attachmentStream.Index, extension, cancellationToken) - .ConfigureAwait(false); - ImageFormat format = extension switch { ".bmp" => ImageFormat.Bmp, @@ -194,6 +208,10 @@ namespace MediaBrowser.Providers.MediaInfo _ => ImageFormat.Jpg }; + string extractedAttachmentPath = + await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, null, attachmentStream.Index, format, cancellationToken) + .ConfigureAwait(false); + return new DynamicImageResponse { Format = format, diff --git a/RSSDP/HttpParserBase.cs b/RSSDP/HttpParserBase.cs index c56249523..6b6c13d99 100644 --- a/RSSDP/HttpParserBase.cs +++ b/RSSDP/HttpParserBase.cs @@ -82,7 +82,7 @@ namespace Rssdp.Infrastructure throw new ArgumentNullException(nameof(versionData)); } - var versionSeparatorIndex = versionData.IndexOf('/'); + var versionSeparatorIndex = versionData.IndexOf('/', StringComparison.Ordinal); if (versionSeparatorIndex <= 0 || versionSeparatorIndex == versionData.Length) { throw new ArgumentException("request header line is invalid. Http Version not supplied or incorrect format.", nameof(versionData)); @@ -101,7 +101,7 @@ namespace Rssdp.Infrastructure { // Header format is // name: value - var headerKeySeparatorIndex = line.IndexOf(":", StringComparison.OrdinalIgnoreCase); + var headerKeySeparatorIndex = line.IndexOf(':', StringComparison.Ordinal); var headerName = line.Substring(0, headerKeySeparatorIndex).Trim(); var headerValue = line.Substring(headerKeySeparatorIndex + 1).Trim(); @@ -172,7 +172,7 @@ namespace Rssdp.Infrastructure else { var segments = headerValue.Split(SeparatorCharacters); - if (headerValue.Contains('"')) + if (headerValue.Contains('"', StringComparison.Ordinal)) { for (int segmentIndex = 0; segmentIndex < segments.Length; segmentIndex++) { diff --git a/jellyfin.ruleset b/jellyfin.ruleset index e14c1c427..469f61021 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -38,8 +38,14 @@ </Rules> <Rules AnalyzerId="Microsoft.CodeAnalysis.NetAnalyzers" RuleNamespace="Microsoft.Design"> + <!-- error on CA1063: Implement IDisposable correctly --> + <Rule Id="CA1063" Action="Error" /> <!-- error on CA1305: Specify IFormatProvider --> <Rule Id="CA1305" Action="Error" /> + <!-- error on CA1307: Specify StringComparison for clarity --> + <Rule Id="CA1307" Action="Error" /> + <!-- error on CA1309: Use ordinal StringComparison --> + <Rule Id="CA1309" Action="Error" /> <!-- error on CA1725: Parameter names should match base declaration --> <Rule Id="CA1725" Action="Error" /> <!-- error on CA1725: Call async methods when in an async method --> diff --git a/tests/Jellyfin.Controller.Tests/BaseItemManagerTests.cs b/tests/Jellyfin.Controller.Tests/BaseItemManagerTests.cs new file mode 100644 index 000000000..edceef4a7 --- /dev/null +++ b/tests/Jellyfin.Controller.Tests/BaseItemManagerTests.cs @@ -0,0 +1,89 @@ +using System; +using MediaBrowser.Controller.BaseItemManager; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Model.Configuration; +using Moq; +using Xunit; + +namespace Jellyfin.Controller.Tests +{ + public class BaseItemManagerTests + { + [Theory] + [InlineData(typeof(Book), "LibraryEnabled", true)] + [InlineData(typeof(Book), "LibraryDisabled", false)] + [InlineData(typeof(MusicArtist), "Enabled", true)] + [InlineData(typeof(MusicArtist), "ServerDisabled", false)] + public void IsMetadataFetcherEnabled_ChecksOptions_ReturnsExpected(Type itemType, string fetcherName, bool expected) + { + BaseItem item = (BaseItem)Activator.CreateInstance(itemType)!; + + var libraryOptions = new LibraryOptions + { + TypeOptions = new[] + { + new TypeOptions + { + Type = "Book", + MetadataFetchers = new[] { "LibraryEnabled" } + } + } + }; + + var serverConfiguration = new ServerConfiguration(); + foreach (var typeConfig in serverConfiguration.MetadataOptions) + { + typeConfig.DisabledMetadataFetchers = new[] { "ServerDisabled" }; + } + + var serverConfigurationManager = new Mock<IServerConfigurationManager>(); + serverConfigurationManager.Setup(scm => scm.Configuration) + .Returns(serverConfiguration); + + var baseItemManager = new BaseItemManager(serverConfigurationManager.Object); + var actual = baseItemManager.IsMetadataFetcherEnabled(item, libraryOptions, fetcherName); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(typeof(Book), "LibraryEnabled", true)] + [InlineData(typeof(Book), "LibraryDisabled", false)] + [InlineData(typeof(MusicArtist), "Enabled", true)] + [InlineData(typeof(MusicArtist), "ServerDisabled", false)] + public void IsImageFetcherEnabled_ChecksOptions_ReturnsExpected(Type itemType, string fetcherName, bool expected) + { + BaseItem item = (BaseItem)Activator.CreateInstance(itemType)!; + + var libraryOptions = new LibraryOptions + { + TypeOptions = new[] + { + new TypeOptions + { + Type = "Book", + ImageFetchers = new[] { "LibraryEnabled" } + } + } + }; + + var serverConfiguration = new ServerConfiguration(); + foreach (var typeConfig in serverConfiguration.MetadataOptions) + { + typeConfig.DisabledImageFetchers = new[] { "ServerDisabled" }; + } + + var serverConfigurationManager = new Mock<IServerConfigurationManager>(); + serverConfigurationManager.Setup(scm => scm.Configuration) + .Returns(serverConfiguration); + + var baseItemManager = new BaseItemManager(serverConfigurationManager.Object); + var actual = baseItemManager.IsImageFetcherEnabled(item, libraryOptions, fetcherName); + + Assert.Equal(expected, actual); + } + } +} diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs index b194e3885..38eac28a2 100644 --- a/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -10,6 +11,7 @@ using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Providers.MediaInfo; +using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; @@ -17,47 +19,25 @@ namespace Jellyfin.Providers.Tests.MediaInfo { public class EmbeddedImageProviderTests { - private static TheoryData<BaseItem> GetSupportedImages_UnsupportedBaseItems_ReturnsEmpty_TestData() - { - return new () - { - new AudioBook(), - new BoxSet(), - new Series(), - new Season(), - }; - } - - [Theory] - [MemberData(nameof(GetSupportedImages_UnsupportedBaseItems_ReturnsEmpty_TestData))] - public void GetSupportedImages_UnsupportedBaseItems_ReturnsEmpty(BaseItem item) - { - var embeddedImageProvider = GetEmbeddedImageProvider(null); - Assert.Empty(embeddedImageProvider.GetSupportedImages(item)); - } - - private static TheoryData<BaseItem, IEnumerable<ImageType>> GetSupportedImages_SupportedBaseItems_ReturnsPopulated_TestData() - { - return new TheoryData<BaseItem, IEnumerable<ImageType>> - { - { new Episode(), new List<ImageType> { ImageType.Primary } }, - { new Movie(), new List<ImageType> { ImageType.Logo, ImageType.Backdrop, ImageType.Primary } }, - }; - } - [Theory] - [MemberData(nameof(GetSupportedImages_SupportedBaseItems_ReturnsPopulated_TestData))] - public void GetSupportedImages_SupportedBaseItems_ReturnsPopulated(BaseItem item, IEnumerable<ImageType> expected) + [InlineData(typeof(AudioBook))] + [InlineData(typeof(BoxSet))] + [InlineData(typeof(Series))] + [InlineData(typeof(Season))] + [InlineData(typeof(Episode), ImageType.Primary)] + [InlineData(typeof(Movie), ImageType.Logo, ImageType.Backdrop, ImageType.Primary)] + public void GetSupportedImages_AnyBaseItem_ReturnsExpected(Type type, params ImageType[] expected) { - var embeddedImageProvider = GetEmbeddedImageProvider(null); + BaseItem item = (BaseItem)Activator.CreateInstance(type)!; + var embeddedImageProvider = new EmbeddedImageProvider(Mock.Of<IMediaEncoder>(), new NullLogger<EmbeddedImageProvider>()); var actual = embeddedImageProvider.GetSupportedImages(item); Assert.Equal(expected.OrderBy(i => i.ToString()), actual.OrderBy(i => i.ToString())); } [Fact] - public async void GetImage_InputWithNoStreams_ReturnsNoImage() + public async void GetImage_NoStreams_ReturnsNoImage() { - var embeddedImageProvider = GetEmbeddedImageProvider(null); + var embeddedImageProvider = new EmbeddedImageProvider(null, new NullLogger<EmbeddedImageProvider>()); var input = GetMovie(new List<MediaAttachment>(), new List<MediaStream>()); @@ -66,136 +46,96 @@ namespace Jellyfin.Providers.Tests.MediaInfo Assert.False(actual.HasImage); } - [Fact] - public async void GetImage_InputWithUnlabeledAttachments_ReturnsNoImage() - { - var embeddedImageProvider = GetEmbeddedImageProvider(null); - - // add an attachment without a filename - has a list to look through but finds nothing - var input = GetMovie( - new List<MediaAttachment> { new () }, - new List<MediaStream>()); - - var actual = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None); - Assert.NotNull(actual); - Assert.False(actual.HasImage); - } - - [Fact] - public async void GetImage_InputWithLabeledAttachments_ReturnsCorrectSelection() + [Theory] + [InlineData("chapter", null, 1, ImageType.Chapter, null)] // unexpected type, nothing found + [InlineData("unmatched", null, 1, ImageType.Primary, null)] // doesn't default on no match + [InlineData("clearlogo.png", null, 1, ImageType.Logo, ImageFormat.Png)] // extract extension from name + [InlineData("backdrop", "image/bmp", 2, ImageType.Backdrop, ImageFormat.Bmp)] // extract extension from mimetype + [InlineData("poster", null, 3, ImageType.Primary, ImageFormat.Jpg)] // default extension to jpg + public async void GetImage_Attachment_ReturnsCorrectSelection(string filename, string mimetype, int targetIndex, ImageType type, ImageFormat? expectedFormat) { - // first tests file extension detection, second uses mimetype, third defaults to jpg - MediaAttachment sampleAttachment1 = new () { FileName = "clearlogo.png", Index = 1 }; - MediaAttachment sampleAttachment2 = new () { FileName = "backdrop", MimeType = "image/bmp", Index = 2 }; - MediaAttachment sampleAttachment3 = new () { FileName = "poster", Index = 3 }; - string targetPath1 = "path1.png"; - string targetPath2 = "path2.bmp"; - string targetPath3 = "path2.jpg"; + var attachments = new List<MediaAttachment>(); + string pathPrefix = "path"; + for (int i = 1; i <= targetIndex; i++) + { + var name = i == targetIndex ? filename : "unmatched"; + attachments.Add(new () + { + FileName = name, + MimeType = mimetype, + Index = i + }); + } var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict); - mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), 1, ".png", CancellationToken.None)) - .Returns(Task.FromResult(targetPath1)); - mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), 2, ".bmp", CancellationToken.None)) - .Returns(Task.FromResult(targetPath2)); - mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), 3, ".jpg", CancellationToken.None)) - .Returns(Task.FromResult(targetPath3)); - var embeddedImageProvider = GetEmbeddedImageProvider(mediaEncoder.Object); - - var input = GetMovie( - new List<MediaAttachment> { sampleAttachment1, sampleAttachment2, sampleAttachment3 }, - new List<MediaStream>()); - - var actualLogo = await embeddedImageProvider.GetImage(input, ImageType.Logo, CancellationToken.None); - Assert.NotNull(actualLogo); - Assert.True(actualLogo.HasImage); - Assert.Equal(targetPath1, actualLogo.Path); - Assert.Equal(ImageFormat.Png, actualLogo.Format); - - var actualBackdrop = await embeddedImageProvider.GetImage(input, ImageType.Backdrop, CancellationToken.None); - Assert.NotNull(actualBackdrop); - Assert.True(actualBackdrop.HasImage); - Assert.Equal(targetPath2, actualBackdrop.Path); - Assert.Equal(ImageFormat.Bmp, actualBackdrop.Format); + mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), It.IsAny<int>(), It.IsAny<ImageFormat>(), It.IsAny<CancellationToken>())) + .Returns<string, string, MediaSourceInfo, MediaStream, int, ImageFormat, CancellationToken>((_, _, _, _, index, ext, _) => Task.FromResult(pathPrefix + index + "." + ext)); + var embeddedImageProvider = new EmbeddedImageProvider(mediaEncoder.Object, new NullLogger<EmbeddedImageProvider>()); - var actualPrimary = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None); - Assert.NotNull(actualPrimary); - Assert.True(actualPrimary.HasImage); - Assert.Equal(targetPath3, actualPrimary.Path); - Assert.Equal(ImageFormat.Jpg, actualPrimary.Format); - } - - [Fact] - public async void GetImage_InputWithUnlabeledEmbeddedImages_BackdropReturnsNoImage() - { - var embeddedImageProvider = GetEmbeddedImageProvider(null); + var input = GetMovie(attachments, new List<MediaStream>()); - var input = GetMovie( - new List<MediaAttachment>(), - new List<MediaStream> { new () { Type = MediaStreamType.EmbeddedImage } }); - - var actual = await embeddedImageProvider.GetImage(input, ImageType.Backdrop, CancellationToken.None); + var actual = await embeddedImageProvider.GetImage(input, type, CancellationToken.None); Assert.NotNull(actual); - Assert.False(actual.HasImage); + if (expectedFormat == null) + { + Assert.False(actual.HasImage); + } + else + { + Assert.True(actual.HasImage); + Assert.Equal(pathPrefix + targetIndex + "." + expectedFormat, actual.Path, StringComparer.OrdinalIgnoreCase); + Assert.Equal(expectedFormat, actual.Format); + } } - [Fact] - public async void GetImage_InputWithUnlabeledEmbeddedImages_PrimaryReturnsImage() + [Theory] + [InlineData("chapter", null, 1, ImageType.Chapter, null)] // unexpected type, nothing found + [InlineData(null, null, 1, ImageType.Backdrop, null)] // no label, can only find primary + [InlineData(null, null, 1, ImageType.Primary, ImageFormat.Jpg)] // no label, finds primary + [InlineData("backdrop", null, 2, ImageType.Backdrop, ImageFormat.Jpg)] // uses label to find index 2, not just pulling first stream + [InlineData("cover", null, 2, ImageType.Primary, ImageFormat.Jpg)] // uses label to find index 2, not just pulling first stream + [InlineData(null, "mjpeg", 1, ImageType.Primary, ImageFormat.Jpg)] + [InlineData(null, "png", 1, ImageType.Primary, ImageFormat.Png)] + [InlineData(null, "gif", 1, ImageType.Primary, ImageFormat.Gif)] + public async void GetImage_Embedded_ReturnsCorrectSelection(string label, string? codec, int targetIndex, ImageType type, ImageFormat? expectedFormat) { - MediaStream sampleStream = new () { Type = MediaStreamType.EmbeddedImage, Index = 1 }; - string targetPath = "path"; - + var streams = new List<MediaStream>(); + for (int i = 1; i <= targetIndex; i++) + { + var comment = i == targetIndex ? label : "unmatched"; + streams.Add(new () + { + Type = MediaStreamType.EmbeddedImage, + Index = i, + Comment = comment, + Codec = codec + }); + } + + var pathPrefix = "path"; var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict); - mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), sampleStream, 1, ".jpg", CancellationToken.None)) - .Returns(Task.FromResult(targetPath)); - var embeddedImageProvider = GetEmbeddedImageProvider(mediaEncoder.Object); + mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), It.IsAny<int>(), It.IsAny<ImageFormat>(), It.IsAny<CancellationToken>())) + .Returns<string, string, MediaSourceInfo, MediaStream, int, ImageFormat, CancellationToken>((_, _, _, stream, index, ext, _) => + { + Assert.Equal(streams[index - 1], stream); + return Task.FromResult(pathPrefix + index + "." + ext); + }); + var embeddedImageProvider = new EmbeddedImageProvider(mediaEncoder.Object, new NullLogger<EmbeddedImageProvider>()); - var input = GetMovie( - new List<MediaAttachment>(), - new List<MediaStream> { sampleStream }); + var input = GetMovie(new List<MediaAttachment>(), streams); - var actual = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None); + var actual = await embeddedImageProvider.GetImage(input, type, CancellationToken.None); Assert.NotNull(actual); - Assert.True(actual.HasImage); - Assert.Equal(targetPath, actual.Path); - Assert.Equal(ImageFormat.Jpg, actual.Format); - } - - [Fact] - public async void GetImage_InputWithLabeledEmbeddedImages_ReturnsCorrectSelection() - { - // primary is second stream to ensure it's not defaulting, backdrop is first - MediaStream sampleStream1 = new () { Type = MediaStreamType.EmbeddedImage, Index = 1, Comment = "backdrop" }; - MediaStream sampleStream2 = new () { Type = MediaStreamType.EmbeddedImage, Index = 2, Comment = "cover" }; - string targetPath1 = "path1.jpg"; - string targetPath2 = "path2.jpg"; - - var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict); - mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), sampleStream1, 1, ".jpg", CancellationToken.None)) - .Returns(Task.FromResult(targetPath1)); - mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), sampleStream2, 2, ".jpg", CancellationToken.None)) - .Returns(Task.FromResult(targetPath2)); - var embeddedImageProvider = GetEmbeddedImageProvider(mediaEncoder.Object); - - var input = GetMovie( - new List<MediaAttachment>(), - new List<MediaStream> { sampleStream1, sampleStream2 }); - - var actualPrimary = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None); - Assert.NotNull(actualPrimary); - Assert.True(actualPrimary.HasImage); - Assert.Equal(targetPath2, actualPrimary.Path); - Assert.Equal(ImageFormat.Jpg, actualPrimary.Format); - - var actualBackdrop = await embeddedImageProvider.GetImage(input, ImageType.Backdrop, CancellationToken.None); - Assert.NotNull(actualBackdrop); - Assert.True(actualBackdrop.HasImage); - Assert.Equal(targetPath1, actualBackdrop.Path); - Assert.Equal(ImageFormat.Jpg, actualBackdrop.Format); - } - - private static EmbeddedImageProvider GetEmbeddedImageProvider(IMediaEncoder? mediaEncoder) - { - return new EmbeddedImageProvider(mediaEncoder); + if (expectedFormat == null) + { + Assert.False(actual.HasImage); + } + else + { + Assert.True(actual.HasImage); + Assert.Equal(pathPrefix + targetIndex + "." + expectedFormat, actual.Path, StringComparer.OrdinalIgnoreCase); + Assert.Equal(expectedFormat, actual.Format); + } } private static Movie GetMovie(List<MediaAttachment> mediaAttachments, List<MediaStream> mediaStreams) |
