diff options
18 files changed, 310 insertions, 121 deletions
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 6e2da9737..20307dd7d 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '8.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@47b3d888fe66b639e431abf22ebca059152f1eea # v3.24.5 + uses: github/codeql-action/init@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@47b3d888fe66b639e431abf22ebca059152f1eea # v3.24.5 + uses: github/codeql-action/autobuild@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@47b3d888fe66b639e431abf22ebca059152f1eea # v3.24.5 + uses: github/codeql-action/analyze@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6 diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index c56349941..b5ccafb86 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -78,12 +78,12 @@ jobs: - openapi-base steps: - name: Download openapi-head - uses: actions/download-artifact@87c55149d96e628cc2ef7e6fc2aab372015aec85 # v4.1.3 + uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 with: name: openapi-head path: openapi-head - name: Download openapi-base - uses: actions/download-artifact@87c55149d96e628cc2ef7e6fc2aab372015aec85 # v4.1.3 + uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 with: name: openapi-base path: openapi-base diff --git a/Directory.Packages.props b/Directory.Packages.props index 1d7ebfaf4..7400edd93 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,7 +16,7 @@ <PackageVersion Include="Diacritics" Version="3.3.27" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> - <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.2.2" /> + <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.2.3" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" /> <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.1" /> <PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" /> @@ -72,7 +72,7 @@ <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.7" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" /> - <PackageVersion Include="Svg.Skia" Version="1.0.0.13" /> + <PackageVersion Include="Svg.Skia" Version="1.0.0.14" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageVersion Include="System.Globalization" Version="4.3.0" /> diff --git a/Jellyfin.Api/Controllers/LyricsController.cs b/Jellyfin.Api/Controllers/LyricsController.cs index 4fccf2cb4..f2b312b47 100644 --- a/Jellyfin.Api/Controllers/LyricsController.cs +++ b/Jellyfin.Api/Controllers/LyricsController.cs @@ -146,13 +146,11 @@ public class LyricsController : BaseJellyfinApiController await using (stream.ConfigureAwait(false)) { await Request.Body.CopyToAsync(stream).ConfigureAwait(false); - var uploadedLyric = await _lyricManager.UploadLyricAsync( - audio, - new LyricResponse - { - Format = format, - Stream = stream - }).ConfigureAwait(false); + var uploadedLyric = await _lyricManager.SaveLyricAsync( + audio, + format, + stream) + .ConfigureAwait(false); if (uploadedLyric is null) { diff --git a/MediaBrowser.Controller/Lyrics/ILyricManager.cs b/MediaBrowser.Controller/Lyrics/ILyricManager.cs index f4376a1ee..1e71b87eb 100644 --- a/MediaBrowser.Controller/Lyrics/ILyricManager.cs +++ b/MediaBrowser.Controller/Lyrics/ILyricManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; @@ -69,12 +70,22 @@ public interface ILyricManager CancellationToken cancellationToken); /// <summary> - /// Upload new lyrics. + /// Saves new lyrics. /// </summary> /// <param name="audio">The audio file the lyrics belong to.</param> - /// <param name="lyricResponse">The lyric response.</param> + /// <param name="format">The lyrics format.</param> + /// <param name="lyrics">The lyrics.</param> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> - Task<LyricDto?> UploadLyricAsync(Audio audio, LyricResponse lyricResponse); + Task<LyricDto?> SaveLyricAsync(Audio audio, string format, string lyrics); + + /// <summary> + /// Saves new lyrics. + /// </summary> + /// <param name="audio">The audio file the lyrics belong to.</param> + /// <param name="format">The lyrics format.</param> + /// <param name="lyrics">The lyrics.</param> + /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> + Task<LyricDto?> SaveLyricAsync(Audio audio, string format, Stream lyrics); /// <summary> /// Get the remote lyrics. diff --git a/MediaBrowser.Providers/Lyric/LyricManager.cs b/MediaBrowser.Providers/Lyric/LyricManager.cs index 60734b89a..f4b18a8c1 100644 --- a/MediaBrowser.Providers/Lyric/LyricManager.cs +++ b/MediaBrowser.Providers/Lyric/LyricManager.cs @@ -155,13 +155,13 @@ public class LyricManager : ILyricManager return null; } - var parsedLyrics = await InternalParseRemoteLyricsAsync(response, cancellationToken).ConfigureAwait(false); + var parsedLyrics = await InternalParseRemoteLyricsAsync(response.Format, response.Stream, cancellationToken).ConfigureAwait(false); if (parsedLyrics is null) { return null; } - await TrySaveLyric(audio, libraryOptions, response).ConfigureAwait(false); + await TrySaveLyric(audio, libraryOptions, response.Format, response.Stream).ConfigureAwait(false); return parsedLyrics; } catch (RateLimitExceededException) @@ -182,19 +182,33 @@ public class LyricManager : ILyricManager } /// <inheritdoc /> - public async Task<LyricDto?> UploadLyricAsync(Audio audio, LyricResponse lyricResponse) + public async Task<LyricDto?> SaveLyricAsync(Audio audio, string format, string lyrics) { ArgumentNullException.ThrowIfNull(audio); - ArgumentNullException.ThrowIfNull(lyricResponse); + ArgumentException.ThrowIfNullOrEmpty(format); + ArgumentException.ThrowIfNullOrEmpty(lyrics); + + var bytes = Encoding.UTF8.GetBytes(lyrics); + using var lyricStream = new MemoryStream(bytes, 0, bytes.Length, false, true); + return await SaveLyricAsync(audio, format, lyricStream).ConfigureAwait(false); + } + + /// <inheritdoc /> + public async Task<LyricDto?> SaveLyricAsync(Audio audio, string format, Stream lyrics) + { + ArgumentNullException.ThrowIfNull(audio); + ArgumentException.ThrowIfNullOrEmpty(format); + ArgumentNullException.ThrowIfNull(lyrics); + var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(audio); - var parsed = await InternalParseRemoteLyricsAsync(lyricResponse, CancellationToken.None).ConfigureAwait(false); + var parsed = await InternalParseRemoteLyricsAsync(format, lyrics, CancellationToken.None).ConfigureAwait(false); if (parsed is null) { return null; } - await TrySaveLyric(audio, libraryOptions, lyricResponse).ConfigureAwait(false); + await TrySaveLyric(audio, libraryOptions, format, lyrics).ConfigureAwait(false); return parsed; } @@ -209,7 +223,7 @@ public class LyricManager : ILyricManager return null; } - return await InternalParseRemoteLyricsAsync(lyricResponse, cancellationToken).ConfigureAwait(false); + return await InternalParseRemoteLyricsAsync(lyricResponse.Format, lyricResponse.Stream, cancellationToken).ConfigureAwait(false); } /// <inheritdoc /> @@ -289,12 +303,12 @@ public class LyricManager : ILyricManager private string GetProviderId(string name) => name.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture); - private async Task<LyricDto?> InternalParseRemoteLyricsAsync(LyricResponse lyricResponse, CancellationToken cancellationToken) + private async Task<LyricDto?> InternalParseRemoteLyricsAsync(string format, Stream lyricStream, CancellationToken cancellationToken) { - lyricResponse.Stream.Seek(0, SeekOrigin.Begin); - using var streamReader = new StreamReader(lyricResponse.Stream, leaveOpen: true); + lyricStream.Seek(0, SeekOrigin.Begin); + using var streamReader = new StreamReader(lyricStream, leaveOpen: true); var lyrics = await streamReader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); - var lyricFile = new LyricFile($"lyric.{lyricResponse.Format}", lyrics); + var lyricFile = new LyricFile($"lyric.{format}", lyrics); foreach (var parser in _lyricParsers) { var parsedLyrics = parser.ParseLyrics(lyricFile); @@ -334,7 +348,7 @@ public class LyricManager : ILyricManager var parsedResults = new List<RemoteLyricInfoDto>(); foreach (var result in searchResults) { - var parsedLyrics = await InternalParseRemoteLyricsAsync(result.Lyrics, cancellationToken).ConfigureAwait(false); + var parsedLyrics = await InternalParseRemoteLyricsAsync(result.Lyrics.Format, result.Lyrics.Stream, cancellationToken).ConfigureAwait(false); if (parsedLyrics is null) { continue; @@ -361,24 +375,23 @@ public class LyricManager : ILyricManager private async Task TrySaveLyric( Audio audio, LibraryOptions libraryOptions, - LyricResponse lyricResponse) + string format, + Stream lyricStream) { var saveInMediaFolder = libraryOptions.SaveLyricsWithMedia; var memoryStream = new MemoryStream(); await using (memoryStream.ConfigureAwait(false)) { - var stream = lyricResponse.Stream; - - await using (stream.ConfigureAwait(false)) + await using (lyricStream.ConfigureAwait(false)) { - stream.Seek(0, SeekOrigin.Begin); - await stream.CopyToAsync(memoryStream).ConfigureAwait(false); + lyricStream.Seek(0, SeekOrigin.Begin); + await lyricStream.CopyToAsync(memoryStream).ConfigureAwait(false); memoryStream.Seek(0, SeekOrigin.Begin); } var savePaths = new List<string>(); - var saveFileName = Path.GetFileNameWithoutExtension(audio.Path) + "." + lyricResponse.Format.ReplaceLineEndings(string.Empty).ToLowerInvariant(); + var saveFileName = Path.GetFileNameWithoutExtension(audio.Path) + "." + format.ReplaceLineEndings(string.Empty).ToLowerInvariant(); if (saveInMediaFolder) { diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index fb86e254f..4d4b59b8c 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -10,6 +10,7 @@ using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; @@ -36,6 +37,7 @@ namespace MediaBrowser.Providers.MediaInfo private readonly ILibraryManager _libraryManager; private readonly IMediaSourceManager _mediaSourceManager; private readonly LyricResolver _lyricResolver; + private readonly ILyricManager _lyricManager; /// <summary> /// Initializes a new instance of the <see cref="AudioFileProber"/> class. @@ -46,13 +48,15 @@ namespace MediaBrowser.Providers.MediaInfo /// <param name="itemRepo">Instance of the <see cref="IItemRepository"/> interface.</param> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="lyricResolver">Instance of the <see cref="LyricResolver"/> interface.</param> + /// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param> public AudioFileProber( ILogger<AudioFileProber> logger, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, ILibraryManager libraryManager, - LyricResolver lyricResolver) + LyricResolver lyricResolver, + ILyricManager lyricManager) { _logger = logger; _mediaEncoder = mediaEncoder; @@ -60,6 +64,7 @@ namespace MediaBrowser.Providers.MediaInfo _libraryManager = libraryManager; _mediaSourceManager = mediaSourceManager; _lyricResolver = lyricResolver; + _lyricManager = lyricManager; } [GeneratedRegex(@"I:\s+(.*?)\s+LUFS")] @@ -107,7 +112,7 @@ namespace MediaBrowser.Providers.MediaInfo cancellationToken.ThrowIfCancellationRequested(); - Fetch(item, result, options, cancellationToken); + await FetchAsync(item, result, options, cancellationToken).ConfigureAwait(false); } var libraryOptions = _libraryManager.GetLibraryOptions(item); @@ -211,7 +216,8 @@ namespace MediaBrowser.Providers.MediaInfo /// <param name="mediaInfo">The <see cref="Model.MediaInfo.MediaInfo"/>.</param> /// <param name="options">The <see cref="MetadataRefreshOptions"/>.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param> - protected void Fetch( + /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> + private async Task FetchAsync( Audio audio, Model.MediaInfo.MediaInfo mediaInfo, MetadataRefreshOptions options, @@ -225,7 +231,7 @@ namespace MediaBrowser.Providers.MediaInfo if (!audio.IsLocked) { - FetchDataFromTags(audio); + await FetchDataFromTags(audio, options).ConfigureAwait(false); } var mediaStreams = new List<MediaStream>(mediaInfo.MediaStreams); @@ -240,9 +246,10 @@ namespace MediaBrowser.Providers.MediaInfo /// Fetches data from the tags. /// </summary> /// <param name="audio">The <see cref="Audio"/>.</param> - private void FetchDataFromTags(Audio audio) + /// <param name="options">The <see cref="MetadataRefreshOptions"/>.</param> + private async Task FetchDataFromTags(Audio audio, MetadataRefreshOptions options) { - var file = TagLib.File.Create(audio.Path); + using var file = TagLib.File.Create(audio.Path); var tagTypes = file.TagTypesOnDisk; Tag? tags = null; @@ -319,14 +326,45 @@ namespace MediaBrowser.Providers.MediaInfo } _libraryManager.UpdatePeople(audio, people); - audio.Artists = performers; - audio.AlbumArtists = albumArtists; + + if (options.ReplaceAllMetadata && performers.Length != 0) + { + audio.Artists = performers; + } + else if (!options.ReplaceAllMetadata + && (audio.Artists is null || audio.Artists.Count == 0)) + { + audio.Artists = performers; + } + + if (options.ReplaceAllMetadata && albumArtists.Length != 0) + { + audio.AlbumArtists = albumArtists; + } + else if (!options.ReplaceAllMetadata + && (audio.AlbumArtists is null || audio.AlbumArtists.Count == 0)) + { + audio.AlbumArtists = albumArtists; + } + } + + if (!audio.LockedFields.Contains(MetadataField.Name)) + { + audio.Name = options.ReplaceAllMetadata || string.IsNullOrEmpty(audio.Name) ? tags.Title : audio.Name; } - audio.Name = tags.Title; - audio.Album = tags.Album; - audio.IndexNumber = Convert.ToInt32(tags.Track); - audio.ParentIndexNumber = Convert.ToInt32(tags.Disc); + if (options.ReplaceAllMetadata) + { + audio.Album = tags.Album; + audio.IndexNumber = Convert.ToInt32(tags.Track); + audio.ParentIndexNumber = Convert.ToInt32(tags.Disc); + } + else + { + audio.Album ??= tags.Album; + audio.IndexNumber ??= Convert.ToInt32(tags.Track); + audio.ParentIndexNumber ??= Convert.ToInt32(tags.Disc); + } if (tags.Year != 0) { @@ -337,14 +375,43 @@ namespace MediaBrowser.Providers.MediaInfo if (!audio.LockedFields.Contains(MetadataField.Genres)) { - audio.Genres = tags.Genres.Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + audio.Genres = options.ReplaceAllMetadata || audio.Genres == null || audio.Genres.Length == 0 + ? tags.Genres.Distinct(StringComparer.OrdinalIgnoreCase).ToArray() + : audio.Genres; } - audio.SetProviderId(MetadataProvider.MusicBrainzArtist, tags.MusicBrainzArtistId); - audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, tags.MusicBrainzReleaseArtistId); - audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, tags.MusicBrainzReleaseId); - audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, tags.MusicBrainzReleaseGroupId); - audio.SetProviderId(MetadataProvider.MusicBrainzTrack, tags.MusicBrainzTrackId); + if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _)) + { + audio.SetProviderId(MetadataProvider.MusicBrainzArtist, tags.MusicBrainzArtistId); + } + + if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _)) + { + audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, tags.MusicBrainzReleaseArtistId); + } + + if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _)) + { + audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, tags.MusicBrainzReleaseId); + } + + if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _)) + { + audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, tags.MusicBrainzReleaseGroupId); + } + + if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out _)) + { + audio.SetProviderId(MetadataProvider.MusicBrainzTrack, tags.MusicBrainzTrackId); + } + + // Save extracted lyrics if they exist, + // and if we are replacing all metadata or the audio doesn't yet have lyrics. + if (!string.IsNullOrWhiteSpace(tags.Lyrics) + && (options.ReplaceAllMetadata || audio.GetMediaStreams().All(s => s.Type != MediaStreamType.Lyric))) + { + await _lyricManager.SaveLyricAsync(audio, "lrc", tags.Lyrics).ConfigureAwait(false); + } } } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 35ea04d21..5d0fccbe1 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -324,20 +324,7 @@ namespace MediaBrowser.Providers.MediaInfo return; } - // Use BD Info if it has multiple m2ts. Otherwise, treat it like a video file and rely more on ffprobe output - int? currentHeight = null; - int? currentWidth = null; - int? currentBitRate = null; - - var videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video); - - // Grab the values that ffprobe recorded - if (videoStream is not null) - { - currentBitRate = videoStream.BitRate; - currentWidth = videoStream.Width; - currentHeight = videoStream.Height; - } + var ffmpegVideoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video); // Fill video properties from the BDInfo result mediaStreams.Clear(); @@ -361,14 +348,16 @@ namespace MediaBrowser.Providers.MediaInfo } } - videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video); + var blurayVideoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video); // Use the ffprobe values if these are empty - if (videoStream is not null) + if (blurayVideoStream is not null && ffmpegVideoStream is not null) { - videoStream.BitRate = videoStream.BitRate.GetValueOrDefault() == 0 ? currentBitRate : videoStream.BitRate; - videoStream.Width = videoStream.Width.GetValueOrDefault() == 0 ? currentWidth : videoStream.Width; - videoStream.Height = videoStream.Height.GetValueOrDefault() == 0 ? currentHeight : videoStream.Height; + // Always use ffmpeg's detected codec since that is what the rest of the codebase expects. + blurayVideoStream.Codec = ffmpegVideoStream.Codec; + blurayVideoStream.BitRate = blurayVideoStream.BitRate.GetValueOrDefault() == 0 ? ffmpegVideoStream.BitRate : blurayVideoStream.BitRate; + blurayVideoStream.Width = blurayVideoStream.Width.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Width; + blurayVideoStream.Height = blurayVideoStream.Height.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Height; } } diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs index 8bb874f0d..8bb8d5bb4 100644 --- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs @@ -13,6 +13,7 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; @@ -64,6 +65,7 @@ namespace MediaBrowser.Providers.MediaInfo /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/>.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> /// <param name="namingOptions">The <see cref="NamingOptions"/>.</param> + /// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param> public ProbeProvider( IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, @@ -77,7 +79,8 @@ namespace MediaBrowser.Providers.MediaInfo ILibraryManager libraryManager, IFileSystem fileSystem, ILoggerFactory loggerFactory, - NamingOptions namingOptions) + NamingOptions namingOptions, + ILyricManager lyricManager) { _logger = loggerFactory.CreateLogger<ProbeProvider>(); _audioResolver = new AudioResolver(loggerFactory.CreateLogger<AudioResolver>(), localization, mediaEncoder, fileSystem, namingOptions); @@ -105,7 +108,8 @@ namespace MediaBrowser.Providers.MediaInfo mediaEncoder, itemRepo, libraryManager, - _lyricResolver); + _lyricResolver, + lyricManager); } /// <inheritdoc /> diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs index a4c6cb47d..18cdba7a0 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs @@ -46,14 +46,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets } /// <inheritdoc /> - public IEnumerable<ImageType> GetSupportedImages(BaseItem item) - { - return new ImageType[] - { - ImageType.Primary, - ImageType.Backdrop - }; - } + public IEnumerable<ImageType> GetSupportedImages(BaseItem item) => + [ + ImageType.Primary, + ImageType.Backdrop, + ImageType.Thumb + ]; /// <inheritdoc /> public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs index bfec48e7c..1696a2c49 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs @@ -47,15 +47,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies } /// <inheritdoc /> - public IEnumerable<ImageType> GetSupportedImages(BaseItem item) - { - return new ImageType[] - { - ImageType.Primary, - ImageType.Backdrop, - ImageType.Logo - }; - } + public IEnumerable<ImageType> GetSupportedImages(BaseItem item) => + [ + ImageType.Primary, + ImageType.Backdrop, + ImageType.Logo, + ImageType.Thumb + ]; /// <inheritdoc /> public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs index 192fb052d..2cb4fe1c1 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs @@ -46,15 +46,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV } /// <inheritdoc /> - public IEnumerable<ImageType> GetSupportedImages(BaseItem item) - { - return new ImageType[] - { - ImageType.Primary, - ImageType.Backdrop, - ImageType.Logo - }; - } + public IEnumerable<ImageType> GetSupportedImages(BaseItem item) => + [ + ImageType.Primary, + ImageType.Backdrop, + ImageType.Logo, + ImageType.Thumb + ]; /// <inheritdoc /> public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs index 82f2c54f1..d704a5f49 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs @@ -591,6 +591,15 @@ namespace MediaBrowser.Providers.Plugins.Tmdb { var image = images[i]; + var imageType = type; + var language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, requestLanguage); + + // Return Backdrops with a language specified (it has text) as Thumb. + if (imageType == ImageType.Backdrop && !string.IsNullOrEmpty(language)) + { + imageType = ImageType.Thumb; + } + yield return new RemoteImageInfo { Url = GetUrl(size, image.FilePath), @@ -598,9 +607,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb VoteCount = image.VoteCount, Width = scaleImage ? null : image.Width, Height = scaleImage ? null : image.Height, - Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, requestLanguage), + Language = language, ProviderName = TmdbUtils.ProviderName, - Type = type, + Type = imageType, RatingType = RatingType.Score }; } diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index d9dceee55..909de8f72 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -21,23 +21,25 @@ namespace Jellyfin.Model.Tests [Theory] // Chrome [InlineData("Chrome", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 - [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Chrome", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] - [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")] + [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] + [InlineData("Chrome", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")] [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported, "Remux")] // #6450 [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 // Firefox [InlineData("Firefox", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 - [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 [InlineData("Firefox", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] - [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")] + [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] + [InlineData("Firefox", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")] [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported, "Remux")] // #6450 [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 @@ -59,18 +61,20 @@ namespace Jellyfin.Model.Tests [InlineData("AndroidPixel", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // Yatse [InlineData("Yatse", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 - [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Yatse", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 [InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] [InlineData("Yatse", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 - [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 + [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Yatse", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 // RokuSSPlus [InlineData("RokuSSPlus", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 - [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 should be DirectPlay - [InlineData("RokuSSPlus", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("RokuSSPlus", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 should be DirectPlay [InlineData("RokuSSPlus", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("RokuSSPlus", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 - [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("RokuSSPlus", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 [InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 // JellyfinMediaPlayer [InlineData("JellyfinMediaPlayer", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 @@ -83,21 +87,24 @@ namespace Jellyfin.Model.Tests [InlineData("JellyfinMediaPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] // #6450 // Chrome-NoHLS [InlineData("Chrome-NoHLS", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 - [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 + [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 [InlineData("Chrome-NoHLS", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Chrome-NoHLS", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "http")] - [InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode", "http")] + [InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "http")] + [InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode", "http")] [InlineData("Chrome-NoHLS", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported, "Remux")] // #6450 [InlineData("Chrome-NoHLS", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Chrome-NoHLS", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 // TranscodeMedia [InlineData("TranscodeMedia", "mp4-h264-aac-vtt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")] - [InlineData("TranscodeMedia", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")] + [InlineData("TranscodeMedia", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "DirectStream", "HLS.mp4")] + [InlineData("TranscodeMedia", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")] [InlineData("TranscodeMedia", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "DirectStream", "HLS.mp4")] [InlineData("TranscodeMedia", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")] - [InlineData("TranscodeMedia", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")] + [InlineData("TranscodeMedia", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "DirectStream", "HLS.mp4")] + [InlineData("TranscodeMedia", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")] [InlineData("TranscodeMedia", "mkv-av1-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "DirectStream", "http")] [InlineData("TranscodeMedia", "mkv-av1-vorbis-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "http")] [InlineData("TranscodeMedia", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "DirectStream", "http")] @@ -459,8 +466,16 @@ namespace Jellyfin.Model.Tests // Audio stream not specified else { - // TODO: Fixme - Assert.All(audioStreams, stream => + bool isDefault = targetAudioStream?.IsDefault == true; + var language = targetAudioStream?.Language; + + // Collect candidate audio streams + var candidateAudioStreams = audioStreams.Where(stream => + { + return isDefault ? stream.IsDefault : (stream.Language == language); + }); + + Assert.All(candidateAudioStreams, stream => { if (!stream.IsExternal) { diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-aac-srt-2600k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-aac-srt-2600k.json index 9d819c4ad..ede1c8d3d 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-aac-srt-2600k.json +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-aac-srt-2600k.json @@ -57,7 +57,7 @@ "BitRate": 164741, "Channels": 2, "SampleRate": 48000, - "IsDefault": true, + "IsDefault": false, "Profile": "LC", "Index": 2, "Score": 203 diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-srt-2600k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-srt-2600k.json index 70bbb9d0d..7ef224957 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-srt-2600k.json +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-srt-2600k.json @@ -57,7 +57,7 @@ "BitRate": 164741, "Channels": 2, "SampleRate": 48000, - "IsDefault": true, + "IsDefault": false, "Profile": "LC", "Index": 2, "Score": 203 diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aac-srt-15200k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aac-srt-15200k.json index 385bb7260..84ed8b52c 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aac-srt-15200k.json +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aac-srt-15200k.json @@ -59,7 +59,7 @@ "BitRate": 164741, "Channels": 2, "SampleRate": 48000, - "IsDefault": true, + "IsDefault": false, "Profile": "LC", "Index": 2, "Score": 203 diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aacDef-srt-15200k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aacDef-srt-15200k.json new file mode 100644 index 000000000..f653313b2 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aacDef-srt-15200k.json @@ -0,0 +1,89 @@ +{ + "Id": "f6eab7118618ab26e61e495a1853481a", + "Path": "/Media/MyVideo-WEBDL-2160p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 6521110016, + "Name": "MyVideo WEBDL-2160p", + "ETag": "a2fb84b618ba2467fe377543f879e9bf", + "RunTimeTicks": 34318510080, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "hevc", + "CodecTag": "hev1", + "Language": "eng", + "ColorSpace": "bt2020nc", + "ColorTransfer": "smpte2084", + "ColorPrimaries": "bt2020", + "TimeBase": "1/16000", + "VideoRange": "HDR", + "DisplayTitle": "4K HEVC HDR", + "BitRate": 14715079, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 2160, + "Width": 3840, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "Main 10", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p10le", + "Level": 150 + }, + { + "Codec": "ac3", + "CodecTag": "ac-3", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - Dolby Digital - 5.1 - Default", + "ChannelLayout": "5.1", + "BitRate": 384000, + "Channels": 6, + "SampleRate": 48000, + "IsDefault": true, + "Index": 1, + "Score": 202 + }, + { + "Codec": "aac", + "CodecTag": "mp4a", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - AAC - Stereo - Default", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "Profile": "LC", + "Index": 2, + "Score": 203 + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 3, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt" + } + ], + "Bitrate": 15201382, + "DefaultAudioStreamIndex": 2, + "DefaultSubtitleStreamIndex": 3 +} |
