diff options
274 files changed, 8900 insertions, 2519 deletions
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ea1d30cdf..a648093ed 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,9 +20,9 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup .NET Core - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v2 with: dotnet-version: '6.0.x' diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index af4d8beb9..730ac7f46 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -23,7 +23,7 @@ jobs: reactions: '+1' - name: Checkout the latest code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 @@ -47,7 +47,7 @@ jobs: reactions: eyes - name: Checkout the latest code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index 3e9346840..e7ac59ea6 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -12,12 +12,12 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: - ref: ${{ github.event.pull_request.head.ref }} + ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} - name: Setup .NET Core - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v2 with: dotnet-version: '6.0.x' - name: Generate openapi.json @@ -37,11 +37,11 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: ref: ${{ github.base_ref }} - name: Setup .NET Core - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v2 with: dotnet-version: '6.0.x' - name: Generate openapi.json diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml index 63ded4140..0504b1c50 100644 --- a/.github/workflows/repo-stale.yaml +++ b/.github/workflows/repo-stale.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@v4.1.0 + - uses: actions/stale@v5 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} days-before-stale: 120 diff --git a/BannedSymbols.txt b/BannedSymbols.txt index dc291e22a..875f7215f 100644 --- a/BannedSymbols.txt +++ b/BannedSymbols.txt @@ -1 +1,4 @@ P:System.Threading.Tasks.Task`1.Result +M:System.Guid.op_Equality(System.Guid,System.Guid) +M:System.Guid.op_Inequality(System.Guid,System.Guid) +M:System.Guid.Equals(System.Object) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 86a8ecc82..87086a728 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -1,5 +1,6 @@ # Jellyfin Contributors + - [1337joe](https://github.com/1337joe) - [97carmine](https://github.com/97carmine) - [Abbe98](https://github.com/Abbe98) - [agrenott](https://github.com/agrenott) @@ -45,6 +46,7 @@ - [Froghut](https://github.com/Froghut) - [fruhnow](https://github.com/fruhnow) - [geilername](https://github.com/geilername) + - [GermanCoding](https://github.com/GermanCoding) - [gnattu](https://github.com/gnattu) - [GodTamIt](https://github.com/GodTamIt) - [grafixeyehero](https://github.com/grafixeyehero) @@ -76,6 +78,7 @@ - [mitchfizz05](https://github.com/mitchfizz05) - [MrTimscampi](https://github.com/MrTimscampi) - [n8225](https://github.com/n8225) + - [Nalsai](https://github.com/Nalsai) - [Narfinger](https://github.com/Narfinger) - [NathanPickard](https://github.com/NathanPickard) - [neilsb](https://github.com/neilsb) @@ -115,6 +118,7 @@ - [ssenart](https://github.com/ssenart) - [stanionascu](https://github.com/stanionascu) - [stevehayles](https://github.com/stevehayles) + - [StollD](https://github.com/StollD) - [SuperSandro2000](https://github.com/SuperSandro2000) - [tbraeutigam](https://github.com/tbraeutigam) - [teacupx](https://github.com/teacupx) @@ -152,6 +156,7 @@ - [MBR-0001](https://github.com/MBR-0001) - [jonas-resch](https://github.com/jonas-resch) - [vgambier](https://github.com/vgambier) + - [MinecraftPlaye](https://github.com/MinecraftPlaye) # Emby Contributors @@ -219,3 +224,4 @@ - [lbenini](https://github.com/lbenini) - [gnuyent](https://github.com/gnuyent) - [Matthew Jones](https://github.com/matthew-jones-uk) + - [Jakob Kukla](https://github.com/jakobkukla) diff --git a/Dockerfile b/Dockerfile index f0090889f..c3038b1d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,10 +22,10 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility" # https://github.com/intel/compute-runtime/releases -ARG GMMLIB_VERSION=21.2.1 -ARG IGC_VERSION=1.0.8517 -ARG NEO_VERSION=21.35.20826 -ARG LEVEL_ZERO_VERSION=1.2.20826 +ARG GMMLIB_VERSION=22.0.2 +ARG IGC_VERSION=1.0.10395 +ARG NEO_VERSION=22.08.22549 +ARG LEVEL_ZERO_VERSION=1.3.22549 # Install dependencies: # mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding. @@ -48,8 +48,7 @@ RUN apt-get update \ && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-gmmlib_${GMMLIB_VERSION}_amd64.deb \ && wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-core_${IGC_VERSION}_amd64.deb \ && wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-opencl_${IGC_VERSION}_amd64.deb \ - && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-opencl_${NEO_VERSION}_amd64.deb \ - && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-ocloc_${NEO_VERSION}_amd64.deb \ + && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-opencl-icd_${NEO_VERSION}_amd64.deb \ && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-level-zero-gpu_${LEVEL_ZERO_VERSION}_amd64.deb \ && dpkg -i *.deb \ && cd .. \ diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index 6803b3b87..6ab5843c1 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -160,7 +160,7 @@ namespace Emby.Dlna.Didl else { var parent = item.DisplayParentId; - if (!parent.Equals(Guid.Empty)) + if (!parent.Equals(default)) { writer.WriteAttributeString("parentID", GetClientId(parent, null)); } @@ -657,7 +657,7 @@ namespace Emby.Dlna.Didl else { var parent = folder.DisplayParentId; - if (parent.Equals(Guid.Empty)) + if (parent.Equals(default)) { writer.WriteAttributeString("parentID", "0"); } diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index 192128c7e..fe78d74ee 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -456,7 +456,7 @@ namespace Emby.Dlna /// <inheritdoc /> public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress) { - var profile = GetDefaultProfile(); + var profile = GetProfile(headers) ?? GetDefaultProfile(); var serverId = _appHost.SystemId; diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj index bf0272e83..472fe140a 100644 --- a/Emby.Dlna/Emby.Dlna.csproj +++ b/Emby.Dlna/Emby.Dlna.csproj @@ -33,7 +33,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 08f639d93..2a535d556 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -362,7 +362,7 @@ namespace Emby.Dlna.Main guid = text.GetMD5(); } - return guid.ToString("N", CultureInfo.InvariantCulture); + return guid.ToString("D", CultureInfo.InvariantCulture); } private void SetProperies(SsdpDevice device, string fullDeviceType) diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs index 7815e9293..8eb90f445 100644 --- a/Emby.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -69,11 +69,11 @@ namespace Emby.Dlna.PlayTo public TransportState TransportState { get; private set; } - public bool IsPlaying => TransportState == TransportState.Playing; + public bool IsPlaying => TransportState == TransportState.PLAYING; - public bool IsPaused => TransportState == TransportState.Paused || TransportState == TransportState.PausedPlayback; + public bool IsPaused => TransportState == TransportState.PAUSED_PLAYBACK; - public bool IsStopped => TransportState == TransportState.Stopped; + public bool IsStopped => TransportState == TransportState.STOPPED; public Action OnDeviceUnavailable { get; set; } @@ -494,7 +494,7 @@ namespace Emby.Dlna.PlayTo cancellationToken: cancellationToken) .ConfigureAwait(false); - TransportState = TransportState.Paused; + TransportState = TransportState.PAUSED_PLAYBACK; RestartTimer(true); } @@ -527,7 +527,7 @@ namespace Emby.Dlna.PlayTo if (transportState.HasValue) { // If we're not playing anything no need to get additional data - if (transportState.Value == TransportState.Stopped) + if (transportState.Value == TransportState.STOPPED) { UpdateMediaInfo(null, transportState.Value); } @@ -556,7 +556,7 @@ namespace Emby.Dlna.PlayTo } // If we're not playing anything make sure we don't get data more often than necessary to keep the Session alive - if (transportState.Value == TransportState.Stopped) + if (transportState.Value == TransportState.STOPPED) { RestartTimerInactive(); } @@ -1229,7 +1229,7 @@ namespace Emby.Dlna.PlayTo } else if (previousMediaInfo == null) { - if (state != TransportState.Stopped) + if (state != TransportState.STOPPED) { OnPlaybackStart(mediaInfo); } diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index e147cb977..e27a8975b 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -174,7 +174,7 @@ namespace Emby.Dlna.PlayTo await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false); // Send a message to the DLNA device to notify what is the next track in the playlist. - var currentItemIndex = _playlist.FindIndex(item => item.StreamInfo.ItemId == streamInfo.ItemId); + var currentItemIndex = _playlist.FindIndex(item => item.StreamInfo.ItemId.Equals(streamInfo.ItemId)); if (currentItemIndex >= 0) { _currentPlaylistIndex = currentItemIndex; @@ -349,7 +349,9 @@ namespace Emby.Dlna.PlayTo { _logger.LogDebug("{0} - Received PlayRequest: {1}", _session.DeviceName, command.PlayCommand); - var user = command.ControllingUserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(command.ControllingUserId); + var user = command.ControllingUserId.Equals(default) + ? null : + _userManager.GetUserById(command.ControllingUserId); var items = new List<BaseItem>(); foreach (var id in command.ItemIds) @@ -392,7 +394,7 @@ namespace Emby.Dlna.PlayTo _playlist.AddRange(playlist); } - if (!command.ControllingUserId.Equals(Guid.Empty)) + if (!command.ControllingUserId.Equals(default)) { _sessionManager.LogSessionActivity( _session.Client, @@ -446,7 +448,9 @@ namespace Emby.Dlna.PlayTo if (info.Item != null && !EnableClientSideSeek(info)) { - var user = !_session.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(_session.UserId) : null; + var user = _session.UserId.Equals(default) + ? null + : _userManager.GetUserById(_session.UserId); var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex); await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false); @@ -764,7 +768,9 @@ namespace Emby.Dlna.PlayTo { var newPosition = GetProgressPositionTicks(info) ?? 0; - var user = !_session.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(_session.UserId) : null; + var user = _session.UserId.Equals(default) + ? null + : _userManager.GetUserById(_session.UserId); var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, newIndex, info.SubtitleStreamIndex); await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false); @@ -793,7 +799,9 @@ namespace Emby.Dlna.PlayTo { var newPosition = GetProgressPositionTicks(info) ?? 0; - var user = !_session.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(_session.UserId) : null; + var user = _session.UserId.Equals(default) + ? null + : _userManager.GetUserById(_session.UserId); var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, newIndex); await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false); @@ -816,7 +824,7 @@ namespace Emby.Dlna.PlayTo const int Interval = 500; var currentWait = 0; - while (_device.TransportState != TransportState.Playing && currentWait < MaxWait) + while (_device.TransportState != TransportState.PLAYING && currentWait < MaxWait) { await Task.Delay(Interval, cancellationToken).ConfigureAwait(false); currentWait += Interval; @@ -949,7 +957,7 @@ namespace Emby.Dlna.PlayTo } } - return Guid.Empty; + return default; } public static StreamParams ParseFromUrl(string url, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager) @@ -964,7 +972,7 @@ namespace Emby.Dlna.PlayTo ItemId = GetItemId(url) }; - if (request.ItemId.Equals(Guid.Empty)) + if (request.ItemId.Equals(default)) { return request; } diff --git a/Emby.Dlna/PlayTo/TransportState.cs b/Emby.Dlna/PlayTo/TransportState.cs index 2058e9dc7..0d6a78438 100644 --- a/Emby.Dlna/PlayTo/TransportState.cs +++ b/Emby.Dlna/PlayTo/TransportState.cs @@ -2,12 +2,15 @@ namespace Emby.Dlna.PlayTo { + /// <summary> + /// Core of the AVTransport service. It defines the conceptually top- + /// level state of the transport, for example, whether it is playing, recording, etc. + /// </summary> public enum TransportState { - Stopped, - Playing, - Transitioning, - PausedPlayback, - Paused + STOPPED, + PLAYING, + TRANSITIONING, + PAUSED_PLAYBACK } } diff --git a/Emby.Drawing/Emby.Drawing.csproj b/Emby.Drawing/Emby.Drawing.csproj index 9bcf6b2ea..f1a6d451b 100644 --- a/Emby.Drawing/Emby.Drawing.csproj +++ b/Emby.Drawing/Emby.Drawing.csproj @@ -32,7 +32,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index eb211050f..961efa48e 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -23,47 +23,61 @@ namespace Emby.Naming.Common { VideoFileExtensions = new[] { - ".m4v", + ".001", + ".3g2", ".3gp", - ".nsv", - ".ts", - ".ty", - ".strm", - ".rm", - ".rmvb", - ".ifo", - ".mov", - ".qt", - ".divx", - ".xvid", - ".bivx", - ".vob", - ".nrg", - ".img", - ".iso", - ".pva", - ".wmv", + ".amv", ".asf", ".asx", - ".ogm", - ".m2v", ".avi", ".bin", + ".bivx", + ".divx", + ".dv", ".dvr-ms", - ".mpg", - ".mpeg", - ".mp4", + ".f4v", + ".fli", + ".flv", + ".ifo", + ".img", + ".iso", + ".m2t", + ".m2ts", + ".m2v", + ".m4v", ".mkv", - ".avc", - ".vp3", - ".svq3", + ".mk3d", + ".mov", + ".mp2", + ".mp4", + ".mpe", + ".mpeg", + ".mpg", + ".mts", + ".mxf", + ".nrg", + ".nsv", ".nuv", + ".ogg", + ".ogm", + ".ogv", + ".pva", + ".qt", + ".rec", + ".rm", + ".rmvb", + ".strm", + ".svq3", + ".tp", + ".ts", + ".ty", ".viv", - ".dv", - ".fli", - ".flv", - ".001", - ".tp" + ".vob", + ".vp3", + ".webm", + ".wmv", + ".wtv", + ".xvid" }; VideoFlagDelimiters = new[] @@ -149,32 +163,20 @@ namespace Emby.Naming.Common SubtitleFileExtensions = new[] { + ".ass", + ".mks", + ".sami", + ".smi", ".srt", ".ssa", - ".ass", - ".sub" - }; - - SubtitleFlagDelimiters = new[] - { - '.' - }; - - SubtitleForcedFlags = new[] - { - "foreign", - "forced" - }; - - SubtitleDefaultFlags = new[] - { - "default" + ".sub", + ".vtt", }; AlbumStackingPrefixes = new[] { - "disc", "cd", + "disc", "disk", "vol", "volume" @@ -182,68 +184,101 @@ namespace Emby.Naming.Common AudioFileExtensions = new[] { - ".nsv", - ".m4a", - ".flac", + ".669", + ".3gp", + ".aa", ".aac", - ".strm", - ".pls", - ".rm", - ".mpa", - ".wav", - ".wma", - ".ogg", - ".opus", - ".mp3", - ".mp2", - ".mod", + ".aax", + ".ac3", + ".act", + ".adp", + ".adplug", + ".adx", + ".afc", ".amf", - ".669", + ".aif", + ".aiff", + ".alac", + ".amr", + ".ape", + ".ast", + ".au", + ".awb", + ".cda", + ".cue", ".dmf", + ".dsf", ".dsm", + ".dsp", + ".dts", + ".dvf", ".far", + ".flac", ".gdm", + ".gsm", + ".gym", + ".hps", ".imf", ".it", ".m15", + ".m4a", + ".m4b", + ".mac", ".med", + ".mka", + ".mmf", + ".mod", + ".mogg", + ".mp2", + ".mp3", + ".mpa", + ".mpc", + ".mpp", + ".mp+", + ".msv", + ".nmf", + ".nsf", + ".nsv", + ".oga", + ".ogg", ".okt", + ".opus", + ".pls", + ".ra", + ".rf64", + ".rm", ".s3m", - ".stm", ".sfx", + ".shn", + ".sid", + ".spc", + ".stm", + ".strm", ".ult", ".uni", - ".xm", - ".sid", - ".ac3", - ".dts", - ".cue", - ".aif", - ".aiff", - ".ape", - ".mac", - ".mpc", - ".mp+", - ".mpp", - ".shn", + ".vox", + ".wav", + ".wma", ".wv", - ".nsf", - ".spc", - ".gym", - ".adplug", - ".adx", - ".dsp", - ".adp", - ".ymf", - ".ast", - ".afc", - ".hps", + ".xm", ".xsp", - ".acc", - ".m4b", - ".oga", - ".dsf", - ".mka" + ".ymf" + }; + + MediaFlagDelimiters = new[] + { + '.' + }; + + MediaForcedFlags = new[] + { + "foreign", + "forced" + }; + + MediaDefaultFlags = new[] + { + "default" }; EpisodeExpressions = new[] @@ -648,45 +683,6 @@ namespace Emby.Naming.Common @"^\s*(?<name>[^ ].*?)\s*$" }; - var extensions = VideoFileExtensions.ToList(); - - extensions.AddRange(new[] - { - ".mkv", - ".m2t", - ".m2ts", - ".img", - ".iso", - ".mk3d", - ".ts", - ".rmvb", - ".mov", - ".avi", - ".mpg", - ".mpeg", - ".wmv", - ".mp4", - ".divx", - ".dvr-ms", - ".wtv", - ".ogm", - ".ogv", - ".asf", - ".m4v", - ".flv", - ".f4v", - ".3gp", - ".webm", - ".mts", - ".m2v", - ".rec", - ".mxf" - }); - - VideoFileExtensions = extensions - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); - MultipleEpisodeExpressions = new[] { @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[eExX](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$", @@ -718,29 +714,29 @@ namespace Emby.Naming.Common public string[] AudioFileExtensions { get; set; } /// <summary> - /// Gets or sets list of album stacking prefixes. + /// Gets or sets list of external media flag delimiters. /// </summary> - public string[] AlbumStackingPrefixes { get; set; } + public char[] MediaFlagDelimiters { get; set; } /// <summary> - /// Gets or sets list of subtitle file extensions. + /// Gets or sets list of external media forced flags. /// </summary> - public string[] SubtitleFileExtensions { get; set; } + public string[] MediaForcedFlags { get; set; } /// <summary> - /// Gets or sets list of subtitles flag delimiters. + /// Gets or sets list of external media default flags. /// </summary> - public char[] SubtitleFlagDelimiters { get; set; } + public string[] MediaDefaultFlags { get; set; } /// <summary> - /// Gets or sets list of subtitle forced flags. + /// Gets or sets list of album stacking prefixes. /// </summary> - public string[] SubtitleForcedFlags { get; set; } + public string[] AlbumStackingPrefixes { get; set; } /// <summary> - /// Gets or sets list of subtitle default flags. + /// Gets or sets list of subtitle file extensions. /// </summary> - public string[] SubtitleDefaultFlags { get; set; } + public string[] SubtitleFileExtensions { get; set; } /// <summary> /// Gets or sets list of episode regular expressions. diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 781c99ae2..ea7309b13 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -52,7 +52,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/Emby.Naming/ExternalFiles/ExternalPathParser.cs b/Emby.Naming/ExternalFiles/ExternalPathParser.cs new file mode 100644 index 000000000..3bde3a1cf --- /dev/null +++ b/Emby.Naming/ExternalFiles/ExternalPathParser.cs @@ -0,0 +1,116 @@ +using System; +using System.IO; +using System.Linq; +using Emby.Naming.Common; +using Jellyfin.Extensions; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Globalization; + +namespace Emby.Naming.ExternalFiles +{ + /// <summary> + /// External media file parser class. + /// </summary> + public class ExternalPathParser + { + private readonly NamingOptions _namingOptions; + private readonly DlnaProfileType _type; + private readonly ILocalizationManager _localizationManager; + + /// <summary> + /// Initializes a new instance of the <see cref="ExternalPathParser"/> class. + /// </summary> + /// <param name="localizationManager">The localization manager.</param> + /// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param> + /// <param name="type">The <see cref="DlnaProfileType"/> of the parsed file.</param> + public ExternalPathParser(NamingOptions namingOptions, ILocalizationManager localizationManager, DlnaProfileType type) + { + _localizationManager = localizationManager; + _namingOptions = namingOptions; + _type = type; + } + + /// <summary> + /// Parse filename and extract information. + /// </summary> + /// <param name="path">Path to file.</param> + /// <param name="extraString">Part of the filename only containing the extra information.</param> + /// <returns>Returns null or an <see cref="ExternalPathParserResult"/> object if parsing is successful.</returns> + public ExternalPathParserResult? ParseFile(string path, string? extraString) + { + if (path.Length == 0) + { + return null; + } + + var extension = Path.GetExtension(path); + if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) + && !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))) + { + return null; + } + + var pathInfo = new ExternalPathParserResult(path); + + if (string.IsNullOrEmpty(extraString)) + { + return pathInfo; + } + + foreach (var separator in _namingOptions.MediaFlagDelimiters) + { + var languageString = extraString; + var titleString = string.Empty; + const int SeparatorLength = 1; + + while (languageString.Length > 0) + { + int lastSeparator = languageString.LastIndexOf(separator); + + if (lastSeparator == -1) + { + break; + } + + string currentSlice = languageString[lastSeparator..]; + string currentSliceWithoutSeparator = currentSlice[SeparatorLength..]; + + if (_namingOptions.MediaDefaultFlags.Any(s => currentSliceWithoutSeparator.Contains(s, StringComparison.OrdinalIgnoreCase))) + { + pathInfo.IsDefault = true; + extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase); + languageString = languageString[..lastSeparator]; + continue; + } + + if (_namingOptions.MediaForcedFlags.Any(s => currentSliceWithoutSeparator.Contains(s, StringComparison.OrdinalIgnoreCase))) + { + pathInfo.IsForced = true; + extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase); + languageString = languageString[..lastSeparator]; + continue; + } + + // Try to translate to three character code + var culture = _localizationManager.FindLanguageInfo(currentSliceWithoutSeparator); + + if (culture != null && pathInfo.Language == null) + { + pathInfo.Language = culture.ThreeLetterISOLanguageName; + extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase); + } + else + { + titleString = currentSlice + titleString; + } + + languageString = languageString[..lastSeparator]; + } + + pathInfo.Title = titleString.Length >= SeparatorLength ? titleString[SeparatorLength..] : null; + } + + return pathInfo; + } + } +} diff --git a/Emby.Naming/Subtitles/SubtitleInfo.cs b/Emby.Naming/ExternalFiles/ExternalPathParserResult.cs index 1fb2e0dc8..1cc773a2e 100644 --- a/Emby.Naming/Subtitles/SubtitleInfo.cs +++ b/Emby.Naming/ExternalFiles/ExternalPathParserResult.cs @@ -1,17 +1,17 @@ -namespace Emby.Naming.Subtitles +namespace Emby.Naming.ExternalFiles { /// <summary> - /// Class holding information about subtitle. + /// Class holding information about external files. /// </summary> - public class SubtitleInfo + public class ExternalPathParserResult { /// <summary> - /// Initializes a new instance of the <see cref="SubtitleInfo"/> class. + /// Initializes a new instance of the <see cref="ExternalPathParserResult"/> class. /// </summary> /// <param name="path">Path to file.</param> - /// <param name="isDefault">Is subtitle default.</param> - /// <param name="isForced">Is subtitle forced.</param> - public SubtitleInfo(string path, bool isDefault, bool isForced) + /// <param name="isDefault">Is default.</param> + /// <param name="isForced">Is forced.</param> + public ExternalPathParserResult(string path, bool isDefault = false, bool isForced = false) { Path = path; IsDefault = isDefault; @@ -31,6 +31,12 @@ namespace Emby.Naming.Subtitles public string? Language { get; set; } /// <summary> + /// Gets or sets the title. + /// </summary> + /// <value>The title.</value> + public string? Title { get; set; } + + /// <summary> /// Gets or sets a value indicating whether this instance is default. /// </summary> /// <value><c>true</c> if this instance is default; otherwise, <c>false</c>.</value> diff --git a/Emby.Naming/Subtitles/SubtitleParser.cs b/Emby.Naming/Subtitles/SubtitleParser.cs deleted file mode 100644 index 5809c512a..000000000 --- a/Emby.Naming/Subtitles/SubtitleParser.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using Emby.Naming.Common; -using Jellyfin.Extensions; - -namespace Emby.Naming.Subtitles -{ - /// <summary> - /// Subtitle Parser class. - /// </summary> - public class SubtitleParser - { - private readonly NamingOptions _options; - - /// <summary> - /// Initializes a new instance of the <see cref="SubtitleParser"/> class. - /// </summary> - /// <param name="options"><see cref="NamingOptions"/> object containing SubtitleFileExtensions, SubtitleDefaultFlags, SubtitleForcedFlags and SubtitleFlagDelimiters.</param> - public SubtitleParser(NamingOptions options) - { - _options = options; - } - - /// <summary> - /// Parse file to determine if is subtitle and <see cref="SubtitleInfo"/>. - /// </summary> - /// <param name="path">Path to file.</param> - /// <returns>Returns null or <see cref="SubtitleInfo"/> object if parsing is successful.</returns> - public SubtitleInfo? ParseFile(string path) - { - if (path.Length == 0) - { - return null; - } - - var extension = Path.GetExtension(path); - if (!_options.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - var flags = GetFlags(path); - var info = new SubtitleInfo( - path, - _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)), - _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase))); - - var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparison.OrdinalIgnoreCase) - && !_options.SubtitleForcedFlags.Contains(i, StringComparison.OrdinalIgnoreCase)) - .ToList(); - - // Should have a name, language and file extension - if (parts.Count >= 3) - { - info.Language = parts[^2]; - } - - return info; - } - - private string[] GetFlags(string path) - { - // Note: the tags need be surrounded be either a space ( ), hyphen -, dot . or underscore _. - - var file = Path.GetFileName(path); - - return file.Split(_options.SubtitleFlagDelimiters, StringSplitOptions.RemoveEmptyEntries); - } - } -} diff --git a/Emby.Notifications/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj index fa7709f2a..e96789d50 100644 --- a/Emby.Notifications/Emby.Notifications.csproj +++ b/Emby.Notifications/Emby.Notifications.csproj @@ -28,7 +28,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/Emby.Notifications/NotificationEntryPoint.cs b/Emby.Notifications/NotificationEntryPoint.cs index a56df7031..668c059b4 100644 --- a/Emby.Notifications/NotificationEntryPoint.cs +++ b/Emby.Notifications/NotificationEntryPoint.cs @@ -112,7 +112,7 @@ namespace Emby.Notifications var userId = e.Argument.UserId; - if (!userId.Equals(Guid.Empty) && !GetOptions().IsEnabledToMonitorUser(type, userId)) + if (!userId.Equals(default) && !GetOptions().IsEnabledToMonitorUser(type, userId)) { return; } diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj index 36419decf..549a4e32d 100644 --- a/Emby.Photos/Emby.Photos.csproj +++ b/Emby.Photos/Emby.Photos.csproj @@ -30,7 +30,7 @@ <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 09429c73f..92a85e862 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -162,7 +162,7 @@ namespace Emby.Server.Implementations.Channels /// <inheritdoc /> public QueryResult<Channel> GetChannelsInternal(ChannelQuery query) { - var user = query.UserId.Equals(Guid.Empty) + var user = query.UserId.Equals(default) ? null : _userManager.GetUserById(query.UserId); @@ -274,7 +274,7 @@ namespace Emby.Server.Implementations.Channels /// <inheritdoc /> public QueryResult<BaseItemDto> GetChannels(ChannelQuery query) { - var user = query.UserId.Equals(Guid.Empty) + var user = query.UserId.Equals(default) ? null : _userManager.GetUserById(query.UserId); @@ -474,7 +474,7 @@ namespace Emby.Server.Implementations.Channels item.ChannelId = id; - if (item.ParentId != parentFolderId) + if (!item.ParentId.Equals(parentFolderId)) { forceUpdate = true; } @@ -715,7 +715,9 @@ namespace Emby.Server.Implementations.Channels // Find the corresponding channel provider plugin var channelProvider = GetChannelProvider(channel); - var parentItem = query.ParentId == Guid.Empty ? channel : _libraryManager.GetItemById(query.ParentId); + var parentItem = query.ParentId.Equals(default) + ? channel + : _libraryManager.GetItemById(query.ParentId); var itemsResult = await GetChannelItems( channelProvider, @@ -726,7 +728,7 @@ namespace Emby.Server.Implementations.Channels cancellationToken) .ConfigureAwait(false); - if (query.ParentId == Guid.Empty) + if (query.ParentId.Equals(default)) { query.Parent = channel; } diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index b5b8fea65..5fc2e39a7 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -265,7 +265,7 @@ namespace Emby.Server.Implementations.Collections { var childItem = _libraryManager.GetItemById(guidId); - var child = collection.LinkedChildren.FirstOrDefault(i => (i.ItemId.HasValue && i.ItemId.Value == guidId) || (childItem != null && string.Equals(childItem.Path, i.Path, StringComparison.OrdinalIgnoreCase))); + var child = collection.LinkedChildren.FirstOrDefault(i => (i.ItemId.HasValue && i.ItemId.Value.Equals(guidId)) || (childItem != null && string.Equals(childItem.Path, i.Path, StringComparison.OrdinalIgnoreCase))); if (child == null) { diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index b3b383bfd..85fa79cba 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -5970,6 +5970,7 @@ AND Type = @InternalPersonType)"); item.LocalizedUndefined = _localization.GetLocalizedString("Undefined"); item.LocalizedDefault = _localization.GetLocalizedString("Default"); item.LocalizedForced = _localization.GetLocalizedString("Forced"); + item.LocalizedExternal = _localization.GetLocalizedString("External"); } return item; diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 2b2190b16..efcfccafe 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1308,7 +1308,7 @@ namespace Emby.Server.Implementations.Dto var allImages = parent.ImageInfos; - if (logoLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && dto.ParentLogoItemId == null) + if (logoLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && dto.ParentLogoItemId is null) { var image = allImages.FirstOrDefault(i => i.Type == ImageType.Logo); @@ -1319,7 +1319,7 @@ namespace Emby.Server.Implementations.Dto } } - if (artLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && dto.ParentArtItemId == null) + if (artLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && dto.ParentArtItemId is null) { var image = allImages.FirstOrDefault(i => i.Type == ImageType.Art); @@ -1330,7 +1330,7 @@ namespace Emby.Server.Implementations.Dto } } - if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && parent is not ICollectionFolder && parent is not UserView) + if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId is null || parent is Series) && parent is not ICollectionFolder && parent is not UserView) { var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb); diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index a5cc125ec..4d43e89c8 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -29,7 +29,7 @@ <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.2" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.3" /> <PackageReference Include="Mono.Nat" Version="3.0.2" /> <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.3" /> <PackageReference Include="sharpcompress" Version="0.30.1" /> @@ -60,7 +60,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index d43996c69..9e35d83aa 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -326,7 +326,7 @@ namespace Emby.Server.Implementations.EntryPoints { var userIds = _sessionManager.Sessions .Select(i => i.UserId) - .Where(i => !i.Equals(Guid.Empty)) + .Where(i => !i.Equals(default)) .Distinct() .ToArray(); diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 34fdfbe8d..e45baedd7 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.EntryPoints { CheckDisposed(); - if (_configurationManager.GetNetworkConfiguration().AutoDiscovery) + if (!_configurationManager.GetNetworkConfiguration().AutoDiscovery) { return Task.CompletedTask; } diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs index bb6041f28..15ab363fe 100644 --- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs @@ -47,7 +47,9 @@ namespace Emby.Server.Implementations.HttpServer.Security { var session = await GetSession(requestContext).ConfigureAwait(false); - return session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId); + return session.UserId.Equals(default) + ? null + : _userManager.GetUserById(session.UserId); } public Task<User?> GetUser(object requestContext) diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 4f8a52f41..399ece7fd 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -704,6 +704,18 @@ namespace Emby.Server.Implementations.IO return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive)); } + /// <inheritdoc /> + public virtual bool DirectoryExists(string path) + { + return Directory.Exists(path); + } + + /// <inheritdoc /> + public virtual bool FileExists(string path) + { + return File.Exists(path); + } + private EnumerationOptions GetEnumerationOptions(bool recursive) { return new EnumerationOptions diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs index 758986945..57c2f1a5e 100644 --- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs @@ -135,7 +135,7 @@ namespace Emby.Server.Implementations.Images protected virtual IEnumerable<string> GetStripCollageImagePaths(BaseItem primaryItem, IEnumerable<BaseItem> items) { - var useBackdrop = primaryItem is CollectionFolder; + var useBackdrop = primaryItem is CollectionFolder || primaryItem is UserView; return items .Select(i => { diff --git a/Emby.Server.Implementations/Images/DynamicImageProvider.cs b/Emby.Server.Implementations/Images/DynamicImageProvider.cs index 575680653..9f9a4902a 100644 --- a/Emby.Server.Implementations/Images/DynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/DynamicImageProvider.cs @@ -84,16 +84,20 @@ namespace Emby.Server.Implementations.Images }).GroupBy(x => x.Id) .Select(x => x.First()); + List<BaseItem> returnItems; if (isUsingCollectionStrip) { - return items + returnItems = items .Where(i => i.HasImage(ImageType.Primary) || i.HasImage(ImageType.Thumb)) .ToList(); + returnItems.Shuffle(); + return returnItems; } - - return items + returnItems = items .Where(i => i.HasImage(ImageType.Primary)) .ToList(); + returnItems.Shuffle(); + return returnItems; } protected override bool Supports(BaseItem item) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index e3be5627f..a9428ae9b 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -46,6 +46,7 @@ using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Episode = MediaBrowser.Controller.Entities.TV.Episode; using EpisodeInfo = Emby.Naming.TV.EpisodeInfo; @@ -99,7 +100,7 @@ namespace Emby.Server.Implementations.Library /// Initializes a new instance of the <see cref="LibraryManager" /> class. /// </summary> /// <param name="appHost">The application host.</param> - /// <param name="logger">The logger.</param> + /// <param name="loggerFactory">The logger factory.</param> /// <param name="taskManager">The task manager.</param> /// <param name="userManager">The user manager.</param> /// <param name="configurationManager">The configuration manager.</param> @@ -115,7 +116,7 @@ namespace Emby.Server.Implementations.Library /// <param name="namingOptions">The naming options.</param> public LibraryManager( IServerApplicationHost appHost, - ILogger<LibraryManager> logger, + ILoggerFactory loggerFactory, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, @@ -131,7 +132,7 @@ namespace Emby.Server.Implementations.Library NamingOptions namingOptions) { _appHost = appHost; - _logger = logger; + _logger = loggerFactory.CreateLogger<LibraryManager>(); _taskManager = taskManager; _userManager = userManager; _configurationManager = configurationManager; @@ -146,7 +147,7 @@ namespace Emby.Server.Implementations.Library _memoryCache = memoryCache; _namingOptions = namingOptions; - _extraResolver = new ExtraResolver(namingOptions); + _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions); _configurationManager.ConfigurationUpdated += ConfigurationUpdated; @@ -755,7 +756,7 @@ namespace Emby.Server.Implementations.Library Path = path }; - if (folder.Id.Equals(Guid.Empty)) + if (folder.Id.Equals(default)) { if (string.IsNullOrEmpty(folder.Path)) { @@ -774,7 +775,7 @@ namespace Emby.Server.Implementations.Library folder = dbItem; } - if (folder.ParentId != rootFolder.Id) + if (!folder.ParentId.Equals(rootFolder.Id)) { folder.ParentId = rootFolder.Id; folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResult(); @@ -1252,7 +1253,7 @@ namespace Emby.Server.Implementations.Library /// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception> public BaseItem GetItemById(Guid id) { - if (id == Guid.Empty) + if (id.Equals(default)) { throw new ArgumentException("Guid can't be empty", nameof(id)); } @@ -1274,7 +1275,7 @@ namespace Emby.Server.Implementations.Library public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent) { - if (query.Recursive && query.ParentId != Guid.Empty) + if (query.Recursive && !query.ParentId.Equals(default)) { var parent = GetItemById(query.ParentId); if (parent != null) @@ -1298,7 +1299,7 @@ namespace Emby.Server.Implementations.Library public int GetCount(InternalItemsQuery query) { - if (query.Recursive && !query.ParentId.Equals(Guid.Empty)) + if (query.Recursive && !query.ParentId.Equals(default)) { var parent = GetItemById(query.ParentId); if (parent != null) @@ -1456,7 +1457,7 @@ namespace Emby.Server.Implementations.Library public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query) { - if (query.Recursive && !query.ParentId.Equals(Guid.Empty)) + if (query.Recursive && !query.ParentId.Equals(default)) { var parent = GetItemById(query.ParentId); if (parent != null) @@ -1512,7 +1513,7 @@ namespace Emby.Server.Implementations.Library private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true) { if (query.AncestorIds.Length == 0 && - query.ParentId.Equals(Guid.Empty) && + query.ParentId.Equals(default) && query.ChannelIds.Count == 0 && query.TopParentIds.Length == 0 && string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) && @@ -1540,7 +1541,7 @@ namespace Emby.Server.Implementations.Library } // Translate view into folders - if (!view.DisplayParentId.Equals(Guid.Empty)) + if (!view.DisplayParentId.Equals(default)) { var displayParent = GetItemById(view.DisplayParentId); if (displayParent != null) @@ -1551,7 +1552,7 @@ namespace Emby.Server.Implementations.Library return Array.Empty<Guid>(); } - if (!view.ParentId.Equals(Guid.Empty)) + if (!view.ParentId.Equals(default)) { var displayParent = GetItemById(view.ParentId); if (displayParent != null) @@ -2153,7 +2154,7 @@ namespace Emby.Server.Implementations.Library return null; } - while (!item.ParentId.Equals(Guid.Empty)) + while (!item.ParentId.Equals(default)) { var parent = item.GetParent(); if (parent == null || parent is AggregateFolder) @@ -2231,7 +2232,9 @@ namespace Emby.Server.Implementations.Library string viewType, string sortName) { - var parentIdString = parentId.Equals(Guid.Empty) ? null : parentId.ToString("N", CultureInfo.InvariantCulture); + var parentIdString = parentId.Equals(default) + ? null + : parentId.ToString("N", CultureInfo.InvariantCulture); var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType ?? string.Empty); var id = GetNewItemId(idValues, typeof(UserView)); @@ -2265,7 +2268,7 @@ namespace Emby.Server.Implementations.Library var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; - if (!refresh && !item.DisplayParentId.Equals(Guid.Empty)) + if (!refresh && !item.DisplayParentId.Equals(default)) { var displayParent = GetItemById(item.DisplayParentId); refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; @@ -2332,7 +2335,7 @@ namespace Emby.Server.Implementations.Library var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; - if (!refresh && !item.DisplayParentId.Equals(Guid.Empty)) + if (!refresh && !item.DisplayParentId.Equals(default)) { var displayParent = GetItemById(item.DisplayParentId); refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; @@ -2365,7 +2368,9 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(name)); } - var parentIdString = parentId.Equals(Guid.Empty) ? null : parentId.ToString("N", CultureInfo.InvariantCulture); + var parentIdString = parentId.Equals(default) + ? null + : parentId.ToString("N", CultureInfo.InvariantCulture); var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType ?? string.Empty); if (!string.IsNullOrEmpty(uniqueId)) { @@ -2409,7 +2414,7 @@ namespace Emby.Server.Implementations.Library var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; - if (!refresh && !item.DisplayParentId.Equals(Guid.Empty)) + if (!refresh && !item.DisplayParentId.Equals(default)) { var displayParent = GetItemById(item.DisplayParentId); refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index eb95977ef..d9a1a5487 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -172,24 +172,16 @@ namespace Emby.Server.Implementations.Library foreach (var source in dynamicMediaSources) { - if (user != null) - { - SetDefaultAudioAndSubtitleStreamIndexes(item, source, user); - } - // Validate that this is actually possible if (source.SupportsDirectStream) { source.SupportsDirectStream = SupportsDirectStream(source.Path, source.Protocol); } - list.Add(source); - } - - if (user != null) - { - foreach (var source in list) + if (user != null) { + SetDefaultAudioAndSubtitleStreamIndexes(item, source, user); + if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) { source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding); @@ -200,6 +192,8 @@ namespace Emby.Server.Implementations.Library source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing); } } + + list.Add(source); } return SortMediaSources(list); @@ -338,6 +332,16 @@ namespace Emby.Server.Implementations.Library foreach (var source in sources) { SetDefaultAudioAndSubtitleStreamIndexes(item, source, user); + + if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + { + source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding); + } + else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + { + source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding); + source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing); + } } } @@ -514,10 +518,10 @@ namespace Emby.Server.Implementations.Library _logger.LogInformation("Live stream opened: {@MediaSource}", mediaSource); var clone = JsonSerializer.Deserialize<MediaSourceInfo>(json, _jsonOptions); - if (!request.UserId.Equals(Guid.Empty)) + if (!request.UserId.Equals(default)) { var user = _userManager.GetUserById(request.UserId); - var item = request.ItemId.Equals(Guid.Empty) + var item = request.ItemId.Equals(default) ? null : _libraryManager.GetItemById(request.ItemId); SetDefaultAudioAndSubtitleStreamIndexes(item, clone, user); diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index d35e74e7b..b2439a87e 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library { return Guid.Empty; } - }).Where(i => !i.Equals(Guid.Empty)).ToArray(); + }).Where(i => !i.Equals(default)).ToArray(); return GetInstantMixFromGenreIds(genreIds, user, dtoOptions); } diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index 9222a9479..3d6b9f3b6 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -12,6 +12,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; +using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Library.Resolvers { @@ -22,8 +23,11 @@ namespace Emby.Server.Implementations.Library.Resolvers public abstract class BaseVideoResolver<T> : MediaBrowser.Controller.Resolvers.ItemResolver<T> where T : Video, new() { - protected BaseVideoResolver(NamingOptions namingOptions) + private readonly ILogger _logger; + + protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions) { + _logger = logger; NamingOptions = namingOptions; } @@ -156,19 +160,26 @@ namespace Emby.Server.Implementations.Library.Resolvers } else { - // use disc-utils, both DVDs and BDs use UDF filesystem - using (var videoFileStream = File.Open(video.Path, FileMode.Open, FileAccess.Read)) - using (UdfReader udfReader = new UdfReader(videoFileStream)) + try { - if (udfReader.DirectoryExists("VIDEO_TS")) - { - video.IsoType = IsoType.Dvd; - } - else if (udfReader.DirectoryExists("BDMV")) + // use disc-utils, both DVDs and BDs use UDF filesystem + using (var videoFileStream = File.Open(video.Path, FileMode.Open, FileAccess.Read)) + using (UdfReader udfReader = new UdfReader(videoFileStream)) { - video.IsoType = IsoType.BluRay; + if (udfReader.DirectoryExists("VIDEO_TS")) + { + video.IsoType = IsoType.Dvd; + } + else if (udfReader.DirectoryExists("BDMV")) + { + video.IsoType = IsoType.BluRay; + } } } + catch (Exception ex) + { + _logger.LogError(ex, "Error opening UDF/ISO image: {Value}", video.Path ?? video.Name); + } } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs index 807913b5d..408e640f9 100644 --- a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs @@ -6,6 +6,7 @@ using Emby.Naming.Video; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.Entities; +using Microsoft.Extensions.Logging; using static Emby.Naming.Video.ExtraRuleResolver; namespace Emby.Server.Implementations.Library.Resolvers @@ -22,12 +23,13 @@ namespace Emby.Server.Implementations.Library.Resolvers /// <summary> /// Initializes a new instance of the <see cref="ExtraResolver"/> class. /// </summary> + /// <param name="logger">The logger.</param> /// <param name="namingOptions">An instance of <see cref="NamingOptions"/>.</param> - public ExtraResolver(NamingOptions namingOptions) + public ExtraResolver(ILogger<ExtraResolver> logger, NamingOptions namingOptions) { _namingOptions = namingOptions; - _trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(namingOptions) }; - _videoResolvers = new IItemResolver[] { new GenericVideoResolver<Video>(namingOptions) }; + _trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions) }; + _videoResolvers = new IItemResolver[] { new GenericVideoResolver<Video>(logger, namingOptions) }; } /// <summary> diff --git a/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs index b8554bd51..5e33b402d 100644 --- a/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs @@ -1,7 +1,8 @@ -#nullable disable +#nullable disable using Emby.Naming.Common; using MediaBrowser.Controller.Entities; +using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Library.Resolvers { @@ -15,9 +16,10 @@ namespace Emby.Server.Implementations.Library.Resolvers /// <summary> /// Initializes a new instance of the <see cref="GenericVideoResolver{T}"/> class. /// </summary> + /// <param name="logger">The logger.</param> /// <param name="namingOptions">The naming options.</param> - public GenericVideoResolver(NamingOptions namingOptions) - : base(namingOptions) + public GenericVideoResolver(ILogger logger, NamingOptions namingOptions) + : base(logger, namingOptions) { } } diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index be1460928..140c4272e 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -17,6 +17,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Library.Resolvers.Movies { @@ -40,9 +41,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies /// Initializes a new instance of the <see cref="MovieResolver"/> class. /// </summary> /// <param name="imageProcessor">The image processor.</param> + /// <param name="logger">The logger.</param> /// <param name="namingOptions">The naming options.</param> - public MovieResolver(IImageProcessor imageProcessor, NamingOptions namingOptions) - : base(namingOptions) + public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions) + : base(logger, namingOptions) { _imageProcessor = imageProcessor; } diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs index be9905647..bfa73af2f 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs @@ -6,6 +6,7 @@ using Emby.Naming.Common; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Entities; +using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Library.Resolvers.TV { @@ -17,9 +18,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// <summary> /// Initializes a new instance of the <see cref="EpisodeResolver"/> class. /// </summary> + /// <param name="logger">The logger.</param> /// <param name="namingOptions">The naming options.</param> - public EpisodeResolver(NamingOptions namingOptions) - : base(namingOptions) + public EpisodeResolver(ILogger<EpisodeResolver> logger, NamingOptions namingOptions) + : base(logger, namingOptions) { } diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index 70d9cbc98..96702d152 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query) { User user = null; - if (query.UserId != Guid.Empty) + if (!query.UserId.Equals(default)) { user = _userManager.GetUserById(query.UserId); } @@ -168,10 +168,10 @@ namespace Emby.Server.Implementations.Library { Fields = new ItemFields[] { - ItemFields.AirTime, - ItemFields.DateCreated, - ItemFields.ChannelInfo, - ItemFields.ParentId + ItemFields.AirTime, + ItemFields.DateCreated, + ItemFields.ChannelInfo, + ItemFields.ParentId } } }; @@ -180,12 +180,12 @@ namespace Emby.Server.Implementations.Library if (searchQuery.IncludeItemTypes.Length == 1 && searchQuery.IncludeItemTypes[0] == BaseItemKind.MusicArtist) { - if (!searchQuery.ParentId.Equals(Guid.Empty)) + if (!searchQuery.ParentId.Equals(default)) { searchQuery.AncestorIds = new[] { searchQuery.ParentId }; + searchQuery.ParentId = Guid.Empty; } - searchQuery.ParentId = Guid.Empty; searchQuery.IncludeItemsByName = true; searchQuery.IncludeItemTypes = Array.Empty<BaseItemKind>(); mediaItems = _libraryManager.GetAllArtists(searchQuery).Items.Select(i => i.Item).ToList(); diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index b00bc72e6..ec411aa3b 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -142,7 +142,7 @@ namespace Emby.Server.Implementations.Library if (index == -1 && i is UserView view - && view.DisplayParentId != Guid.Empty) + && !view.DisplayParentId.Equals(default)) { index = Array.IndexOf(orders, view.DisplayParentId); } @@ -214,7 +214,7 @@ namespace Emby.Server.Implementations.Library } else { - var current = list.FirstOrDefault(i => i.Item1 != null && i.Item1.Id == container.Id); + var current = list.FirstOrDefault(i => i.Item1 != null && i.Item1.Id.Equals(container.Id)); if (current != null) { @@ -244,7 +244,7 @@ namespace Emby.Server.Implementations.Library var parents = new List<BaseItem>(); - if (!parentId.Equals(Guid.Empty)) + if (!parentId.Equals(default)) { var parentItem = _libraryManager.GetItemById(parentId); if (parentItem is Channel) diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index bba584854..2753cf177 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -2024,7 +2024,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV await writer.WriteElementStringAsync(null, "genre", null, genre).ConfigureAwait(false); } - var people = item.Id.Equals(Guid.Empty) ? new List<PersonInfo>() : _libraryManager.GetPeople(item); + var people = item.Id.Equals(default) ? new List<PersonInfo>() : _libraryManager.GetPeople(item); var directors = people .Where(i => IsPersonType(i, PersonType.Director)) @@ -2382,7 +2382,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { string channelId = seriesTimer.RecordAnyChannel ? null : seriesTimer.ChannelId; - if (string.IsNullOrWhiteSpace(channelId) && !parent.ChannelId.Equals(Guid.Empty)) + if (string.IsNullOrWhiteSpace(channelId) && !parent.ChannelId.Equals(default)) { if (!tempChannelCache.TryGetValue(parent.ChannelId, out LiveTvChannel channel)) { @@ -2441,7 +2441,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { string channelId = null; - if (!programInfo.ChannelId.Equals(Guid.Empty)) + if (!programInfo.ChannelId.Equals(default)) { if (!tempChannelCache.TryGetValue(programInfo.ChannelId, out LiveTvChannel channel)) { diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs index fbce7af2d..c09f9cf8d 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -456,7 +456,7 @@ namespace Emby.Server.Implementations.LiveTv info.Id = timer.ExternalId; } - if (!dto.ChannelId.Equals(Guid.Empty) && string.IsNullOrEmpty(info.ChannelId)) + if (!dto.ChannelId.Equals(default) && string.IsNullOrEmpty(info.ChannelId)) { var channel = _libraryManager.GetItemById(dto.ChannelId); @@ -522,7 +522,7 @@ namespace Emby.Server.Implementations.LiveTv info.Id = timer.ExternalId; } - if (!dto.ChannelId.Equals(Guid.Empty) && string.IsNullOrEmpty(info.ChannelId)) + if (!dto.ChannelId.Equals(default) && string.IsNullOrEmpty(info.ChannelId)) { var channel = _libraryManager.GetItemById(dto.ChannelId); diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index 71a29e3cb..97c2e6e30 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -176,7 +176,9 @@ namespace Emby.Server.Implementations.LiveTv public QueryResult<BaseItem> GetInternalChannels(LiveTvChannelQuery query, DtoOptions dtoOptions, CancellationToken cancellationToken) { - var user = query.UserId == Guid.Empty ? null : _userManager.GetUserById(query.UserId); + var user = query.UserId.Equals(default) + ? null + : _userManager.GetUserById(query.UserId); var topFolder = GetInternalLiveTvFolder(cancellationToken); @@ -1268,7 +1270,7 @@ namespace Emby.Server.Implementations.LiveTv { cancellationToken.ThrowIfCancellationRequested(); - if (itemId.Equals(Guid.Empty)) + if (itemId.Equals(default)) { // Somehow some invalid data got into the db. It probably predates the boundary checking continue; @@ -1528,7 +1530,9 @@ namespace Emby.Server.Implementations.LiveTv public QueryResult<BaseItemDto> GetRecordings(RecordingQuery query, DtoOptions options) { - var user = query.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(query.UserId); + var user = query.UserId.Equals(default) + ? null + : _userManager.GetUserById(query.UserId); RemoveFields(options); @@ -1587,7 +1591,7 @@ namespace Emby.Server.Implementations.LiveTv if (!string.IsNullOrEmpty(query.ChannelId)) { var guid = new Guid(query.ChannelId); - timers = timers.Where(i => guid == _tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId)); + timers = timers.Where(i => _tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId).Equals(guid)); } if (!string.IsNullOrEmpty(query.SeriesTimerId)) @@ -1595,7 +1599,7 @@ namespace Emby.Server.Implementations.LiveTv var guid = new Guid(query.SeriesTimerId); timers = timers - .Where(i => _tvDtoService.GetInternalSeriesTimerId(i.Item1.SeriesTimerId) == guid); + .Where(i => _tvDtoService.GetInternalSeriesTimerId(i.Item1.SeriesTimerId).Equals(guid)); } if (!string.IsNullOrEmpty(query.Id)) @@ -1657,7 +1661,7 @@ namespace Emby.Server.Implementations.LiveTv if (!string.IsNullOrEmpty(query.ChannelId)) { var guid = new Guid(query.ChannelId); - timers = timers.Where(i => guid == _tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId)); + timers = timers.Where(i => _tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId).Equals(guid)); } if (!string.IsNullOrEmpty(query.SeriesTimerId)) @@ -1665,7 +1669,7 @@ namespace Emby.Server.Implementations.LiveTv var guid = new Guid(query.SeriesTimerId); timers = timers - .Where(i => _tvDtoService.GetInternalSeriesTimerId(i.Item1.SeriesTimerId) == guid); + .Where(i => _tvDtoService.GetInternalSeriesTimerId(i.Item1.SeriesTimerId).Equals(guid)); } if (!string.IsNullOrEmpty(query.Id)) diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 9bab3b9a9..502c562a5 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -120,5 +120,8 @@ "Forced": "Forçat", "Default": "Defecto", "TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després d’escanejar la biblioteca o fer altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.", - "TaskOptimizeDatabase": "Optimitzar la base de dades" + "TaskOptimizeDatabase": "Optimitzar la base de dades", + "TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot durar molt de temps.", + "TaskKeyframeExtractor": "Extractor de fotogrames clau", + "External": "Extern" } diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index 7ee8d1040..943fc651f 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -120,5 +120,8 @@ "Forced": "Vynucené", "Default": "Výchozí", "TaskOptimizeDatabaseDescription": "Zmenší databázi a odstraní prázdné místo. Spuštění této úlohy po skenování knihovny či jiných změnách databáze může zlepšit výkon.", - "TaskOptimizeDatabase": "Optimalizovat databázi" + "TaskOptimizeDatabase": "Optimalizovat databázi", + "TaskKeyframeExtractorDescription": "Vytahuje klíčové snímky ze souborů videa za účelem vytváření přesnějších seznamů přehrávání HLS. Tento úkol může trvat velmi dlouho.", + "TaskKeyframeExtractor": "Vytahovač klíčových snímků", + "External": "Externí" } diff --git a/Emby.Server.Implementations/Localization/Core/cy.json b/Emby.Server.Implementations/Localization/Core/cy.json index 7fc27e18a..331c3d678 100644 --- a/Emby.Server.Implementations/Localization/Core/cy.json +++ b/Emby.Server.Implementations/Localization/Core/cy.json @@ -15,7 +15,7 @@ "Folders": "Ffolderi", "Favorites": "Ffefrynnau", "LabelRunningTimeValue": "Amser rhedeg: {0}", - "TaskOptimizeDatabase": "Cronfa ddata Optimeiddio", + "TaskOptimizeDatabase": "Optimeiddio cronfa ddata", "TaskRefreshChannels": "Adnewyddu Sianeli", "TaskRefreshPeople": "Adnewyddu Pobl", "TasksChannelsCategory": "Sianeli Internet", @@ -54,5 +54,73 @@ "Undefined": "Heb ddiffiniad", "TvShows": "Rhaglenni teledu", "HeaderLiveTV": "Teledu Byw", - "User": "Defnyddiwr" + "User": "Defnyddiwr", + "TaskCleanLogsDescription": "Dileu ffeiliau log sy'n fwy na {0} diwrnod oed.", + "TaskCleanLogs": "Glanhau ffolder log", + "TaskRefreshLibraryDescription": "Sganio'ch llyfrgell gyfryngau am ffeiliau newydd ac yn adnewyddu metaddata.", + "TaskRefreshLibrary": "Sganwich Llyfrgell Cyfryngau", + "TaskCleanActivityLogDescription": "Yn dileu cofnodion log gweithgaredd sy'n hŷn na'r oedran a nodwyd.", + "TaskCleanActivityLog": "Glanhau Log Gweithgaredd", + "SubtitleDownloadFailureFromForItem": "Methodd is-deitlau lawrlwytho o {0} ar gyfer {1}", + "NotificationOptionPluginError": "Methodd ategyn", + "NotificationOptionAudioPlaybackStopped": "Stopiwyd chwarae sain", + "NotificationOptionAudioPlayback": "Dechreuwyd chwarae sain", + "MessageServerConfigurationUpdated": "Mae gosodiadau gweinydd wedi'i ddiweddaru", + "MessageNamedServerConfigurationUpdatedWithValue": "Mae adran gosodiadau gweinydd {0} wedi'i diweddaru", + "FailedLoginAttemptWithUserName": "Cais mewngofnodi wedi methu gan {0}", + "ValueHasBeenAddedToLibrary": "{0} wedi'i hychwanegu at eich llyfrgell gyfryngau", + "UserStoppedPlayingItemWithValues": "{0} wedi gorffen chwarae {1} ar {2}", + "UserStartedPlayingItemWithValues": "{0} yn chwarae {1} ar {2}", + "UserPolicyUpdatedWithName": "Polisi defnyddiwr wedi'i newid ar gyfer {0}", + "UserPasswordChangedWithName": "Cyfrinair wedi'i newid ar gyfer defnyddiwr {0}", + "UserOnlineFromDevice": "Mae {0} ar-lein o {1}", + "UserOfflineFromDevice": "Mae {0} wedi datgysylltu o {1}", + "UserLockedOutWithName": "Mae defnyddiwr {0} wedi'i gloi allan", + "UserDownloadingItemWithValues": "Mae {0} yn lawrlwytho {1}", + "UserDeletedWithName": "Defnyddiwr {0} wedi'i ddileu", + "UserCreatedWithName": "Defnyddiwr {0} wedi'i greu", + "StartupEmbyServerIsLoading": "Gweinydd Jellyfin yn llwytho. Triwch eto mewn ychydig.", + "ServerNameNeedsToBeRestarted": "Mae angen ailddechrau {0}", + "PluginUpdatedWithName": "{0} wedi'i ddiweddaru", + "PluginUninstalledWithName": "{0} wedi'i ddadosod", + "PluginInstalledWithName": "{0} wedi'i osod", + "NotificationOptionVideoPlaybackStopped": "Stopiwyd chwarae fideo", + "NotificationOptionVideoPlayback": "Dechreuwyd chwarae fideo", + "NotificationOptionUserLockedOut": "Defnyddiwr wedi'i gloi allan", + "NotificationOptionTaskFailed": "Methwyd cyflawni y dasg a drefnwyd", + "NotificationOptionServerRestartRequired": "Mae angen ailgychwyn y gweinydd", + "NotificationOptionPluginUpdateInstalled": "Diweddariad ategyn wedi'i osod", + "NotificationOptionPluginUninstalled": "Ategyn wedi'i ddadosod", + "NotificationOptionPluginInstalled": "Ategyn wedi'i osod", + "NotificationOptionNewLibraryContent": "Cynnwys newydd ar gael", + "NotificationOptionCameraImageUploaded": "Llun camera wedi'i huwchlwytho", + "NotificationOptionApplicationUpdateInstalled": "Diweddariad ap wedi'i osod", + "NotificationOptionApplicationUpdateAvailable": "Diweddariad ap ar gael", + "NewVersionIsAvailable": "Mae fersiwn diweddarach o'r gweinydd Jellyfin ar gael.", + "NameInstallFailed": "Gosodiad {0} wedi methu", + "MessageApplicationUpdatedTo": "Gweinydd Jellyfin wedi'i ddiweddaru i {0}", + "MessageApplicationUpdated": "Gweinydd Jellyfin wedi'i ddiweddaru", + "LabelIpAddressValue": "Cyfeiriad IP: {0}", + "ItemRemovedWithName": "{0} wedi'i dynnu o'r llyfrgell", + "ItemAddedWithName": "{0} wedi'i adio i'r llyfrgell", + "HeaderRecordingGroups": "Grwpiau Recordio", + "HeaderFavoriteSongs": "Ffefryn Ganeuon", + "HeaderFavoriteShows": "Ffefryn Shoeau", + "HeaderFavoriteEpisodes": "Ffefryn Rhaglenni", + "TaskDownloadMissingSubtitlesDescription": "Chwilio'r rhyngrwyd am is-deitlau coll yn seiliedig ar gosodiadau metaddata.", + "TaskDownloadMissingSubtitles": "Lawrlwytho isdeitlau coll", + "TaskCleanTranscodeDescription": "Dileu ffeiliau trawsgodio fwy nag un diwrnod oed.", + "External": "Allanol", + "TaskKeyframeExtractorDescription": "Echdynnu fframiau o ffeiliau fideo i greu rhestrau chwarae HLS mwy manwl gywir. Gall y dasg hon redeg am amser hir.", + "TaskKeyframeExtractor": "Echdynnwr ffram-allwedd", + "TaskOptimizeDatabaseDescription": "Crynhoi cronfa ddata ac yn cwtogi'r gofod rhydd. Gallai rhedeg y dasg hon ar ôl sganio'r llyfrgell neu wneud newidiadau eraill sy'n addasu'r cronfa ddata wella perfformiad.", + "TaskRefreshChannelsDescription": "Diweddaru gwybodaeth sianeli rhyngrwyd.", + "TaskCleanTranscode": "Gwaghau Ffolder Trawsgodau", + "TaskUpdatePluginsDescription": "Lawrlwytho ac yn gosod diweddariadau ar gyfer ategion sydd wedi'u gosod i'w diweddaru'n awtomatig.", + "TaskUpdatePlugins": "Diweddaru Ategion", + "TaskRefreshPeopleDescription": "Yn diweddaru metaddata ar gyfer actorion a chyfarwyddwyr yn eich llyfrgell gyfryngau.", + "TaskRefreshChapterImagesDescription": "Creu mân-luniau ar gyfer fideos sydd â phenodau.", + "TaskRefreshChapterImages": "Echdynnu Lluniau Pennod", + "TaskCleanCacheDescription": "Dileu ffeiliau cache nad oes eu hangen ar y system mwyach.", + "TaskCleanCache": "Gwaghau Ffolder Cache" } diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index 115f36e7c..722b81c8a 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -120,5 +120,8 @@ "Forced": "Erzwungen", "Default": "Standard", "TaskOptimizeDatabaseDescription": "Komprimiert die Datenbank und trimmt den freien Speicherplatz. Die Ausführung dieser Aufgabe nach dem Scannen der Bibliothek oder nach anderen Änderungen, die Datenbankänderungen implizieren, kann die Leistung verbessern.", - "TaskOptimizeDatabase": "Datenbank optimieren" + "TaskOptimizeDatabase": "Datenbank optimieren", + "TaskKeyframeExtractorDescription": "Extrahiere Keyframes aus Videodateien, um präzisere HLS-Playlisten zu erzeugen. Diese Aufgabe kann sehr lange dauern.", + "TaskKeyframeExtractor": "Keyframe Extraktor", + "External": "Extern" } diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index 9952c05ca..acf42f38e 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -120,5 +120,8 @@ "Forced": "Εξαναγκασμένο", "Default": "Προεπιλογή", "TaskOptimizeDatabaseDescription": "Συμπιέζει τη βάση δεδομένων και δημιουργεί ελεύθερο χώρο. Η εκτέλεση αυτής της εργασίας μετά τη σάρωση της βιβλιοθήκης ή την πραγματοποίηση άλλων αλλαγών που συνεπάγονται τροποποιήσεις της βάσης δεδομένων μπορεί να βελτιώσει την απόδοση.", - "TaskOptimizeDatabase": "Βελτιστοποίηση βάσης δεδομένων" + "TaskOptimizeDatabase": "Βελτιστοποίηση βάσης δεδομένων", + "TaskKeyframeExtractorDescription": "Εξάγει τα βασικά καρέ από αρχεία βίντεο για να δημιουργήσει πιο ακριβείς HLS λίστες αναπαραγωγής. Αυτή η εργασία μπορεί να διαρκέσει πολλή ώρα.", + "TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο", + "External": "Εξωτερικό" } diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json index add578376..862410c54 100644 --- a/Emby.Server.Implementations/Localization/Core/en-GB.json +++ b/Emby.Server.Implementations/Localization/Core/en-GB.json @@ -120,5 +120,8 @@ "Forced": "Forced", "Default": "Default", "TaskOptimizeDatabaseDescription": "Compacts database and truncates free space. Running this task after scanning the library or doing other changes that imply database modifications might improve performance.", - "TaskOptimizeDatabase": "Optimise database" + "TaskOptimizeDatabase": "Optimise database", + "TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.", + "TaskKeyframeExtractor": "Keyframe Extractor", + "External": "External" } diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json index 568a8e447..d8c33d51b 100644 --- a/Emby.Server.Implementations/Localization/Core/en-US.json +++ b/Emby.Server.Implementations/Localization/Core/en-US.json @@ -12,6 +12,7 @@ "Default": "Default", "DeviceOfflineWithName": "{0} has disconnected", "DeviceOnlineWithName": "{0} is connected", + "External": "External", "FailedLoginAttemptWithUserName": "Failed login try from {0}", "Favorites": "Favorites", "Folders": "Folders", @@ -119,5 +120,7 @@ "TaskDownloadMissingSubtitles": "Download missing subtitles", "TaskDownloadMissingSubtitlesDescription": "Searches the internet for missing subtitles based on metadata configuration.", "TaskOptimizeDatabase": "Optimize database", - "TaskOptimizeDatabaseDescription": "Compacts database and truncates free space. Running this task after scanning the library or doing other changes that imply database modifications might improve performance." + "TaskOptimizeDatabaseDescription": "Compacts database and truncates free space. Running this task after scanning the library or doing other changes that imply database modifications might improve performance.", + "TaskKeyframeExtractor": "Keyframe Extractor", + "TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time." } diff --git a/Emby.Server.Implementations/Localization/Core/eo.json b/Emby.Server.Implementations/Localization/Core/eo.json index 8abf7fa66..0b595c2ca 100644 --- a/Emby.Server.Implementations/Localization/Core/eo.json +++ b/Emby.Server.Implementations/Localization/Core/eo.json @@ -119,5 +119,8 @@ "HeaderRecordingGroups": "Rikordadaj Grupoj", "FailedLoginAttemptWithUserName": "Malsukcesa ensaluta provo de {0}", "CameraImageUploadedFrom": "Nova kamera bildo estis alŝutita de {0}", - "AuthenticationSucceededWithUserName": "{0} sukcese aŭtentikigis" + "AuthenticationSucceededWithUserName": "{0} sukcese aŭtentikigis", + "TaskKeyframeExtractorDescription": "Eltiras ĉefkadrojn el videodosieroj por krei pli precizajn HLS-ludlistojn. Ĉi tiu tasko povas funkcii dum longa tempo.", + "TaskKeyframeExtractor": "Eltiri Ĉefkadrojn", + "External": "Ekstera" } diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json index 432814dac..80ae16c5c 100644 --- a/Emby.Server.Implementations/Localization/Core/es-MX.json +++ b/Emby.Server.Implementations/Localization/Core/es-MX.json @@ -120,5 +120,7 @@ "Forced": "Forzado", "Default": "Predeterminado", "TaskOptimizeDatabase": "Optimizar base de datos", - "TaskOptimizeDatabaseDescription": "Compacta la base de datos y trunca el espacio libre. Puede mejorar el rendimiento si se realiza esta tarea después de escanear la biblioteca o después de realizar otros cambios que impliquen modificar la base de datos." + "TaskOptimizeDatabaseDescription": "Compacta la base de datos y trunca el espacio libre. Puede mejorar el rendimiento si se realiza esta tarea después de escanear la biblioteca o después de realizar otros cambios que impliquen modificar la base de datos.", + "TaskKeyframeExtractorDescription": "Extrae los cuadros clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar un buen rato.", + "TaskKeyframeExtractor": "Extractor de Cuadros Clave" } diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index f8c69712e..4918f468b 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -120,5 +120,7 @@ "Forced": "Forzado", "Default": "Predeterminado", "TaskOptimizeDatabase": "Optimizar la base de datos", - "TaskOptimizeDatabaseDescription": "Compacta y libera el espacio libre en la base de datos. Ejecutar esta tarea tras escanear la biblioteca o hacer cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento." + "TaskOptimizeDatabaseDescription": "Compacta y libera el espacio libre en la base de datos. Ejecutar esta tarea tras escanear la biblioteca o hacer cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento.", + "TaskKeyframeExtractorDescription": "Extrae los fotogramas clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar mucho tiempo.", + "TaskKeyframeExtractor": "Extractor de Fotogramas Clave" } diff --git a/Emby.Server.Implementations/Localization/Core/eu.json b/Emby.Server.Implementations/Localization/Core/eu.json new file mode 100644 index 000000000..dfedce7b3 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/eu.json @@ -0,0 +1,120 @@ +{ + "ValueSpecialEpisodeName": "Berezia - {0}", + "Sync": "Sinkronizatu", + "Songs": "Abestiak", + "Shows": "Serieak", + "Playlists": "Erreprodukzio-zerrendak", + "Photos": "Argazkiak", + "MusicVideos": "Bideo musikalak", + "Movies": "Filmak", + "HeaderContinueWatching": "Ikusten jarraitu", + "HeaderAlbumArtists": "Albumeko artistak", + "Genres": "Generoak", + "Folders": "Karpetak", + "Favorites": "Gogokoak", + "Default": "Lehenetsia", + "Collections": "Bildumak", + "Channels": "Kanalak", + "Books": "Liburuak", + "Artists": "Artistak", + "Albums": "Albumak", + "TaskOptimizeDatabase": "Datu basea optimizatu", + "TaskDownloadMissingSubtitlesDescription": "Metadataren konfigurazioan oinarrituta falta diren azpitituluak bilatzen ditu interneten.", + "TaskDownloadMissingSubtitles": "Falta diren azpitituluak deskargatu", + "TaskRefreshChannelsDescription": "Internet kanalen informazioa eguneratu.", + "TaskRefreshChannels": "Kanalak eguneratu", + "TaskCleanTranscodeDescription": "Egun bat baino zaharragoak diren transcode fitxategiak ezabatzen ditu.", + "TaskCleanTranscode": "Transcode direktorioa garbitu", + "TaskUpdatePluginsDescription": "Automatikoki eguneratzeko konfiguratutako pluginen eguneraketak deskargatu eta instalatzen ditu.", + "TaskUpdatePlugins": "Pluginak eguneratu", + "TaskRefreshPeopleDescription": "Zure liburutegiko aktore eta zuzendarien metadata eguneratzen du.", + "TaskRefreshPeople": "Jendea eguneratu", + "TaskCleanLogsDescription": "{0} egun baino zaharragoak diren log fitxategiak ezabatzen ditu.", + "TaskCleanLogs": "Log direktorioa garbitu", + "TaskRefreshLibraryDescription": "Zure multimedia liburutegia eskaneatzen du fitxategi berriak eta metadatak eguneratzeko.", + "TaskRefreshLibrary": "Multimedia Liburutegia eskaneatu", + "TaskRefreshChapterImagesDescription": "Kapituluak dituzten bideoen miniaturak sortzen ditu.", + "TaskRefreshChapterImages": "Kapituluen irudiak erauzi", + "TaskCleanCacheDescription": "Sistemak behar ez dituen cache fitxategiak ezabatzen ditu.", + "TaskCleanCache": "Cache Directorioa garbitu", + "TaskCleanActivityLogDescription": "Konfiguratuta data baino zaharragoak diren log-ak ezabatu.", + "TaskCleanActivityLog": "Erabilera Log-a garbitu", + "TasksChannelsCategory": "Internet Kanalak", + "TasksApplicationCategory": "Aplikazioa", + "TasksLibraryCategory": "Liburutegia", + "TasksMaintenanceCategory": "Mantenua", + "VersionNumber": "Bertsioa {0}", + "ValueHasBeenAddedToLibrary": "{0} zure multimedia liburutegian gehitu da", + "UserStoppedPlayingItemWithValues": "{0}-ek {1} ikusteaz bukatu du {2}-(a)n", + "UserStartedPlayingItemWithValues": "{0} {1} ikusten ari da {2}-(a)n", + "UserPolicyUpdatedWithName": "{0} Erabiltzailearen politikak aldatu dira", + "UserPasswordChangedWithName": "{0} Erabiltzailearen pasahitza aldatu da", + "UserOnlineFromDevice": "{0} online dago {1}-tik", + "UserOfflineFromDevice": "{0} {1}-tik deskonektatu da", + "UserLockedOutWithName": "{0} Erabiltzailea blokeatu da", + "UserDownloadingItemWithValues": "{1} {0}-tik deskargatzen", + "UserDeletedWithName": "{0} Erabiltzailea ezabatu da", + "UserCreatedWithName": "{0} Erabiltzailea sortu da", + "User": "Erabiltzailea", + "Undefined": "Ezezaguna", + "TvShows": "TB showak", + "System": "Sistema", + "SubtitleDownloadFailureFromForItem": "{1}-en azpitutuluak {0} deskargatzean huts egin du", + "StartupEmbyServerIsLoading": "Jellyfin zerbitzaria kargatzen. Saiatu berriro beranduxeago.", + "ServerNameNeedsToBeRestarted": "{0} berrabiarazi behar da", + "ScheduledTaskStartedWithName": "{0} hasi da", + "ScheduledTaskFailedWithName": "{0} huts egin du", + "PluginUpdatedWithName": "{0} eguneratu da", + "PluginUninstalledWithName": "{0} desinstalatu da", + "PluginInstalledWithName": "{0} instalatu da", + "Plugin": "Plugin", + "NotificationOptionVideoPlaybackStopped": "Bideoa geldituta", + "NotificationOptionVideoPlayback": "Bideoa martxan", + "NotificationOptionUserLockedOut": "Erabiltzailea blokeatua", + "NotificationOptionTaskFailed": "Programatutako atazak huts egin du", + "NotificationOptionServerRestartRequired": "Zerbitzaria berrabiarazi behar da", + "NotificationOptionPluginUpdateInstalled": "Pluginaren eguneraketa instalatua", + "NotificationOptionPluginUninstalled": "Plugina desinstalatua", + "NotificationOptionPluginInstalled": "Plugina instalatua", + "NotificationOptionPluginError": "Pluginak huts egin du", + "NotificationOptionNewLibraryContent": "Eduki berria gehitua", + "NotificationOptionInstallationFailed": "Instalazioak huts egin du", + "NotificationOptionCameraImageUploaded": "Kamerako irudia igota", + "NotificationOptionAudioPlaybackStopped": "Audioa gelditua", + "NotificationOptionAudioPlayback": "Audioa martxan", + "NotificationOptionApplicationUpdateInstalled": "Aplikazioaren eguneraketa instalatua", + "NotificationOptionApplicationUpdateAvailable": "Aplikazioaren eguneraketa eskuragarri", + "NewVersionIsAvailable": "Jellyfin zerbitzariaren bertsio berria deskargatzeko eskuragarri dago.", + "NameSeasonUnknown": "Denboraldi ezezaguna", + "NameSeasonNumber": "{0} Denboraldia", + "NameInstallFailed": "{0} instalazioak huts egin du", + "Music": "Musika", + "MixedContent": "Denetariko edukia", + "MessageServerConfigurationUpdated": "Zerbitzariaren konfigurazioa eguneratu da", + "MessageNamedServerConfigurationUpdatedWithValue": "Zerbitzariaren konfigurazio {0} atala eguneratu da", + "MessageApplicationUpdatedTo": "Jellyfin zerbitzaria {0}-ra eguneratu da", + "MessageApplicationUpdated": "Jellyfin zerbitzaria eguneratu da", + "Latest": "Azkena", + "LabelRunningTimeValue": "Denbora martxan: {0}", + "LabelIpAddressValue": "IP helbidea: {0}", + "ItemRemovedWithName": "{0} liburutegitik ezabatu da", + "ItemAddedWithName": "{0} liburutegira gehitu da", + "HomeVideos": "Etxeko bideoak", + "HeaderNextUp": "Hurrengoa", + "HeaderLiveTV": "Zuzeneko TB", + "HeaderFavoriteSongs": "Gogoko abestiak", + "HeaderFavoriteShows": "Gogoko showak", + "HeaderFavoriteEpisodes": "Gogoko atalak", + "HeaderFavoriteArtists": "Gogoko artistak", + "HeaderFavoriteAlbums": "Gogoko albumak", + "Forced": "Behartuta", + "FailedLoginAttemptWithUserName": "Login egiten akatsa, saiatu hemen {0}", + "External": "Kanpokoa", + "DeviceOnlineWithName": "{0} konektatu da", + "DeviceOfflineWithName": "{0} deskonektatu da", + "ChapterNameValue": "{0} Kapitulua", + "CameraImageUploadedFrom": "{0}-tik kamera irudi berri bat igo da", + "AuthenticationSucceededWithUserName": "{0} ongi autentifikatu da", + "Application": "Aplikazioa", + "AppDeviceValues": "App: {0}, Gailua: {1}" +} diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json index 6960ff007..026648af4 100644 --- a/Emby.Server.Implementations/Localization/Core/fa.json +++ b/Emby.Server.Implementations/Localization/Core/fa.json @@ -119,5 +119,9 @@ "TaskCleanActivityLogDescription": "ورودیهای قدیمیتر از سن تنظیم شده در سیاهه فعالیت را حذف میکند.", "TaskCleanActivityLog": "پاکسازی سیاهه فعالیت", "Undefined": "تعریف نشده", - "TaskOptimizeDatabase": "بهینه سازی پایگاه داده" + "TaskOptimizeDatabase": "بهینه سازی پایگاه داده", + "TaskOptimizeDatabaseDescription": "فشرده سازی پایگاه داده و باز کردن فضای آزاد.اجرای این گزینه بعد از اسکن کردن کتابخانه یا تغییرات دیگر که روی پایگاه داده تأثیر میگذارند میتواند کارایی را بهبود ببخشد.", + "TaskKeyframeExtractorDescription": "فریم های کلیدی را از فایل های ویدئویی استخراج می کند تا لیست های پخش HLS دقیق تری ایجاد کند. این کار ممکن است برای مدت طولانی اجرا شود.", + "TaskKeyframeExtractor": "استخراج کننده فریم کلیدی", + "External": "خارجی" } diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json index 4a1f4f1d5..8675ab2e6 100644 --- a/Emby.Server.Implementations/Localization/Core/fi.json +++ b/Emby.Server.Implementations/Localization/Core/fi.json @@ -119,5 +119,8 @@ "TaskCleanActivityLog": "Tyhjennä toimintahistoria", "Undefined": "Määrittelemätön", "TaskOptimizeDatabaseDescription": "Tiivistää ja puhdistaa tietokannan. Tämän toiminnon suorittaminen kirjastojen skannauksen tai muiden tietokantaan liittyvien muutoksien jälkeen voi parantaa suorituskykyä.", - "TaskOptimizeDatabase": "Optimoi tietokanta" + "TaskOptimizeDatabase": "Optimoi tietokanta", + "TaskKeyframeExtractorDescription": "Purkaa videotiedostojen avainkuvat tarkempien HLS-toistolistojen luomiseksi. Tehtävä saattaa kestää huomattavan pitkään.", + "TaskKeyframeExtractor": "Avainkuvien purkain", + "External": "Ulkoinen" } diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index e56ae6071..2a329e74d 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -120,5 +120,7 @@ "Forced": "Forcé", "Default": "Par défaut", "TaskOptimizeDatabaseDescription": "Réduit les espaces vides/inutiles et compacte la base de données. Utiliser cette fonction après une mise à jour de la bibliothèque ou toute autre modification de la base de données peut améliorer les performances du serveur.", - "TaskOptimizeDatabase": "Optimiser la base de données" + "TaskOptimizeDatabase": "Optimiser la base de données", + "TaskKeyframeExtractorDescription": "Extrait les images clés des fichiers vidéo pour créer des listes de lecture HLS plus précises. Cette tâche peut durer très longtemps.", + "TaskKeyframeExtractor": "Extracteur d'image clé" } diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json index 85de5925e..781cfcfa2 100644 --- a/Emby.Server.Implementations/Localization/Core/hi.json +++ b/Emby.Server.Implementations/Localization/Core/hi.json @@ -61,5 +61,10 @@ "LabelRunningTimeValue": "चलने का समय: {0}", "ItemAddedWithName": "{0} को लाइब्रेरी में जोड़ा गया", "Inherit": "इनहेरिट", - "NotificationOptionVideoPlaybackStopped": "चलचित्र रुका हुआ" + "NotificationOptionVideoPlaybackStopped": "चलचित्र रुका हुआ", + "PluginUninstalledWithName": "{0} अनइंस्टॉल हुए", + "PluginInstalledWithName": "{0} इंस्टॉल हुए", + "Plugin": "प्लग-इन", + "Playlists": "प्लेलिस्ट", + "Photos": "तस्वीरें" } diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index acde84aaf..c7f2f9c85 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -120,5 +120,8 @@ "Forced": "Kényszerített", "Default": "Alapértelmezett", "TaskOptimizeDatabaseDescription": "Tömöríti az adatbázist és csonkolja a szabad helyet. A feladat futtatása a könyvtár beolvasása után, vagy egyéb, adatbázis-módosítást igénylő változtatások végrehajtása javíthatja a teljesítményt.", - "TaskOptimizeDatabase": "Adatbázis optimalizálása" + "TaskOptimizeDatabase": "Adatbázis optimalizálása", + "TaskKeyframeExtractor": "Kulcskockák kibontása", + "TaskKeyframeExtractorDescription": "Kulcskockákat bont ki a videofájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat.", + "External": "Külső" } diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index 4c4de4999..2feff0922 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -120,5 +120,7 @@ "Forced": "Forzato", "Default": "Predefinito", "TaskOptimizeDatabaseDescription": "Compatta Database e tronca spazi liberi. Eseguire questa azione dopo la scansione o dopo aver fatto altri cambiamenti inerenti il database potrebbe aumentarne la performance.", - "TaskOptimizeDatabase": "Ottimizza Database" + "TaskOptimizeDatabase": "Ottimizza Database", + "TaskKeyframeExtractor": "Estrattore di Keyframe", + "TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori playlist HLS. Questa procedura potrebbe richiedere molto tempo." } diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json index 2588f1e8c..8cab706be 100644 --- a/Emby.Server.Implementations/Localization/Core/ja.json +++ b/Emby.Server.Implementations/Localization/Core/ja.json @@ -119,5 +119,8 @@ "Forced": "強制", "Default": "デフォルト", "TaskOptimizeDatabaseDescription": "データベースをコンパクトにして、空き領域を切り詰めます。メディアライブラリのスキャン後でこのタスクを実行するとパフォーマンスが向上する可能性があります。", - "TaskOptimizeDatabase": "データベースの最適化" + "TaskOptimizeDatabase": "データベースの最適化", + "TaskKeyframeExtractorDescription": "より正確なHLSプレイリストを作成するため、動画ファイルからキーフレームを抽出する。この処理には時間がかかる場合があります。", + "TaskKeyframeExtractor": "キーフレーム抽出", + "External": "外部" } diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json index 1b4a18deb..c5a93cb96 100644 --- a/Emby.Server.Implementations/Localization/Core/kk.json +++ b/Emby.Server.Implementations/Localization/Core/kk.json @@ -120,5 +120,8 @@ "TaskCleanCacheDescription": "Jüiede qajet emes keştelgen faildardy joiady.", "TaskCleanActivityLogDescription": "Äreket jūrnalyndağy teñşelgen jasynan asqan jazbalary joiady.", "TaskOptimizeDatabaseDescription": "Derekqordy qysyp, bos oryndy qysqartady. Būl tapsyrmany tasyğyşhanany skanerlegennen keiın nemese derekqorğa meñzeitın basqa özgertuler ıstelgennen keiın oryndau önımdılıktı damytuy mümkın.", - "TaskOptimizeDatabase": "Derekqordy oñtailandyru" + "TaskOptimizeDatabase": "Derekqordy oñtailandyru", + "TaskKeyframeExtractorDescription": "Naqtyraq HLS oynatu tızımderın jasau üşın beinefaildardan negızgı kadrlardy şyğarady. Būl tapsyrma ūzaq uaqytqa sozyluy mümkın.", + "TaskKeyframeExtractor": "Negızgı kadrlardy şyğaru", + "External": "Syrtqy" } diff --git a/Emby.Server.Implementations/Localization/Core/kn.json b/Emby.Server.Implementations/Localization/Core/kn.json new file mode 100644 index 000000000..3c8c38ed4 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/kn.json @@ -0,0 +1,7 @@ +{ + "TaskDownloadMissingSubtitlesDescription": "ಮೆಟಾಡೇಟಾ ಕಾನ್ಫಿಗರೇಶನ್ ಆಧಾರದ ಮೇಲೆ ಕಾಣೆಯಾದ ಉಪಶೀರ್ಷಿಕೆಗಳಿಗಾಗಿ ಅಂತರ್ಜಾಲದಲ್ಲಿ ಹುಡುಕುತ್ತದೆ.", + "TaskOptimizeDatabase": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಆಪ್ಟಿಮೈಜ್ ಮಾಡಿ", + "TaskOptimizeDatabaseDescription": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಕಾಂಪ್ಯಾಕ್ಟ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮುಕ್ತ ಜಾಗವನ್ನು ಮೊಟಕುಗೊಳಿಸುತ್ತದೆ. ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿದ ನಂತರ ಈ ಕಾರ್ಯವನ್ನು ನಡೆಸುವುದು ಅಥವಾ ಡೇಟಾಬೇಸ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಸೂಚಿಸುವ ಇತರ ಬದಲಾವಣೆಗಳನ್ನು ಮಾಡುವುದರಿಂದ ಕಾರ್ಯಕ್ಷಮತೆಯನ್ನು ಸುಧಾರಿಸಬಹುದು.", + "TaskKeyframeExtractor": "ಕೀಫ್ರೇಮ್ ಎಕ್ಸ್ಟ್ರಾಕ್ಟರ್", + "TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್ಗಳಿಂದ ಕೀಫ್ರೇಮ್ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು." +} diff --git a/Emby.Server.Implementations/Localization/Core/mr.json b/Emby.Server.Implementations/Localization/Core/mr.json index fdb4171b5..5aad4b0ed 100644 --- a/Emby.Server.Implementations/Localization/Core/mr.json +++ b/Emby.Server.Implementations/Localization/Core/mr.json @@ -58,5 +58,47 @@ "Application": "अॅप्लिकेशन", "AppDeviceValues": "अॅप: {0}, यंत्र: {1}", "Collections": "संग्रह", - "ChapterNameValue": "धडा {0}" + "ChapterNameValue": "धडा {0}", + "TaskDownloadMissingSubtitlesDescription": "नसलेल्या उपशिर्षकांचा मेटाडॅटा कॉन्फिग्युरेशनप्रमाणे इन्टरनेटवर शोध घेतो.", + "TaskRefreshChannelsDescription": "इन्टरनेट वाहिन्यांची माहिती ताजी करतो.", + "TaskUpdatePluginsDescription": "आपोआप अपडेट करण्यासाठी कॉन्फिगर केलेल्या प्लगइनसाठी अपडेट डाउनलोड करून इन्स्टॉल करतो.", + "TaskRefreshChannels": "वाहिन्या ताज्या करा", + "TaskRefreshPeopleDescription": "आपल्या माध्यम संग्रहातील अभिनेत्यांचा व दिग्दर्शकांचा मेटाडॅटा ताजा करतो.", + "TaskRefreshPeople": "लोकांची माहिती ताजी करा", + "TaskRefreshLibraryDescription": "माध्यम संग्रह स्कॅन करून नवीन फायली शोधतो व मेटाडॅटा ताजे करतो.", + "TaskRefreshLibrary": "माध्यम संग्रह स्कॅन करा", + "TaskRefreshChapterImagesDescription": "अध्याय असलेल्या व्हिडियोंसाठी थंबनेल चित्र बनवतो.", + "TaskRefreshChapterImages": "अध्याय चित्र काढून घ्या", + "TasksMaintenanceCategory": "देखरेख", + "ValueHasBeenAddedToLibrary": "{0} हे तुमच्या माध्यम संग्रहात जोडण्यात आले आहे", + "UserStoppedPlayingItemWithValues": "{0} यांचं {2} वर {1} पूर्णपणे प्ले करून झालं आहे", + "UserStartedPlayingItemWithValues": "{0} हे {2} वर {1} प्ले करत आहे", + "UserDownloadingItemWithValues": "{0} हे {1} डाउनलोड करत आहे", + "System": "प्रणाली", + "Undefined": "अव्याख्यात", + "Sync": "सिंक", + "ServerNameNeedsToBeRestarted": "{0} याला बंद करून पुन्हा सुरू करायची गरज आहे", + "SubtitleDownloadFailureFromForItem": "{0} येथून {1} यासाठी उपशिर्षक डाउनलोड करण्यात अपयश", + "ScheduledTaskStartedWithName": "{0} सुरू झाले", + "ScheduledTaskFailedWithName": "{0} अपयशी झाले", + "ProviderValue": "पुरवणारा: {0}", + "PluginUpdatedWithName": "{0} अपडेट केले", + "PluginUninstalledWithName": "{0} अनिन्स्टॉल केले", + "PluginInstalledWithName": "{0} इन्स्टॉल केले", + "NotificationOptionVideoPlaybackStopped": "व्हिडियो प्लेबॅक बंद केले", + "NotificationOptionVideoPlayback": "व्हिडियो प्लेबॅक सुरू केले", + "NotificationOptionTaskFailed": "अनुसूचित कार्यात अपयश", + "NotificationOptionServerRestartRequired": "सर्व्हर बंद करून पुन्हा सुरू करावा लागेल", + "NotificationOptionPluginUpdateInstalled": "प्लगइन अपडेट इन्स्टॉल झाले", + "NotificationOptionPluginUninstalled": "प्लगइन अनिन्स्टॉल झाले", + "NotificationOptionPluginInstalled": "प्लगइन इन्स्टॉल झाले", + "NotificationOptionPluginError": "प्लगइनमध्ये अपयश", + "NotificationOptionNewLibraryContent": "नवीन सामग्री जोडली गेली", + "NotificationOptionInstallationFailed": "इन्स्टॉल करण्यात अपयश", + "NotificationOptionAudioPlayback": "ऑडियो प्लेबॅक सुरू झाले", + "NotificationOptionAudioPlaybackStopped": "ऑडियो प्लेबॅक बंद झाले", + "MixedContent": "मिश्रित सामग्री", + "LabelRunningTimeValue": "चालू काल: {0}", + "HeaderContinueWatching": "बघणे चालू ठेवा", + "Default": "डीफॉल्ट" } diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json index 81aa996d9..506c14fdc 100644 --- a/Emby.Server.Implementations/Localization/Core/pr.json +++ b/Emby.Server.Implementations/Localization/Core/pr.json @@ -1,7 +1,17 @@ { - "Books": "Libros", - "AuthenticationSucceededWithUserName": "{0} autentificado correctamente", + "Books": "Scrolls", + "AuthenticationSucceededWithUserName": "{0} passed yer trial", "Artists": "Artistas", "Songs": "Shantees", - "Albums": "Ships" + "Albums": "Tomes", + "Photos": "Paintings", + "NotificationOptionUserLockedOut": "Crewmate sent to the brig", + "HeaderContinueWatching": "Continue Yer Journey", + "Folders": "Chests", + "Application": "Captain", + "DeviceOnlineWithName": "{0} joined yer crew", + "DeviceOfflineWithName": "{0} abandoned ship", + "AppDeviceValues": "Captain: {0}, Ship: {1}", + "CameraImageUploadedFrom": "Yer looking glass has glimpsed another painting from {0}", + "Collections": "Barrels" } diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json index 8af5449a7..57bfd1792 100644 --- a/Emby.Server.Implementations/Localization/Core/ro.json +++ b/Emby.Server.Implementations/Localization/Core/ro.json @@ -119,5 +119,7 @@ "Forced": "Forțat", "Default": "Implicit", "TaskOptimizeDatabaseDescription": "Compactează baza de date și trunchiază spațiul liber. Rularea acestei sarcini după scanarea bibliotecii sau după efectuarea altor modificări care implică modificări ale bazei de date poate îmbunătăți performanța.", - "TaskOptimizeDatabase": "Optimizează baza de date" + "TaskOptimizeDatabase": "Optimizează baza de date", + "TaskKeyframeExtractorDescription": "Extrage cadrele cheie din fișierele video pentru a crea liste de redare HLS mai precise. Această sarcină poate rula o perioadă lungă de timp.", + "External": "Extern" } diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index dc3793f1b..f489597a1 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -120,5 +120,8 @@ "Forced": "Форсир-ые", "Default": "По умолчанию", "TaskOptimizeDatabaseDescription": "Сжимает базу данных и вырезает свободные места. Выполнение этой задачи после сканирования библиотеки или внесения других изменений, предполагающих модификации базы данных, может повысить производительность.", - "TaskOptimizeDatabase": "Оптимизация базы данных" + "TaskOptimizeDatabase": "Оптимизация базы данных", + "TaskKeyframeExtractorDescription": "Извлекаются ключевые кадры из видеофайлов для создания более точных списков плей-листов HLS. Эта задача может выполняться в течение длительного времени.", + "TaskKeyframeExtractor": "Извлечение ключевых кадров", + "External": "Внешние" } diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json index 37da7d5ab..7502969a6 100644 --- a/Emby.Server.Implementations/Localization/Core/sk.json +++ b/Emby.Server.Implementations/Localization/Core/sk.json @@ -120,5 +120,8 @@ "Forced": "Vynútené", "Default": "Predvolené", "TaskOptimizeDatabaseDescription": "Zmenší databázu a odstráni prázdne miesto. Spustenie tejto úlohy po skenovaní knižnice alebo po iných zmenách zahŕňajúcich úpravy databáze môže zlepšiť výkon.", - "TaskOptimizeDatabase": "Optimalizovať databázu" + "TaskOptimizeDatabase": "Optimalizovať databázu", + "TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS playlistov. Táto úloha môže trvať dlhšiu dobu.", + "TaskKeyframeExtractor": "Extraktor kľúčových snímkov", + "External": "Externé" } diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json index 5d05361b0..2b9f9e232 100644 --- a/Emby.Server.Implementations/Localization/Core/sv.json +++ b/Emby.Server.Implementations/Localization/Core/sv.json @@ -92,33 +92,36 @@ "ValueHasBeenAddedToLibrary": "{0} har lagts till i ditt mediebibliotek", "ValueSpecialEpisodeName": "Specialavsnitt - {0}", "VersionNumber": "Version {0}", - "TaskDownloadMissingSubtitlesDescription": "Söker på internet efter saknade undertexter baserad på metadatas konfiguration.", - "TaskDownloadMissingSubtitles": "Ladda ned saknade undertexter", + "TaskDownloadMissingSubtitlesDescription": "Söker på internet efter saknade undertexter baserat på metadata-konfiguration.", + "TaskDownloadMissingSubtitles": "Ladda ner saknade undertexter", "TaskRefreshChannelsDescription": "Uppdaterar information för internetkanaler.", - "TaskRefreshChannels": "Uppdatera kanaler", - "TaskCleanTranscodeDescription": "Raderar omkodningsfiler som är mer än en dag gamla.", - "TaskCleanTranscode": "Töm omkodningskatalog", + "TaskRefreshChannels": "Uppdatera Kanaler", + "TaskCleanTranscodeDescription": "Raderar omkodningsfiler äldre än en dag.", + "TaskCleanTranscode": "Rensa Omkodningskatalog", "TaskUpdatePluginsDescription": "Laddar ned och installerar uppdateringar till insticksprogram som är konfigurerade att uppdateras automatiskt.", - "TaskUpdatePlugins": "Uppdatera insticksprogram", + "TaskUpdatePlugins": "Uppdatera Insticksprogram", "TaskRefreshPeopleDescription": "Uppdaterar metadata för skådespelare och regissörer i ditt mediabibliotek.", "TaskCleanLogsDescription": "Raderar loggfiler som är mer än {0} dagar gamla.", - "TaskCleanLogs": "Töm loggkatalog", - "TaskRefreshLibraryDescription": "Söker igenom ditt mediabibliotek efter nya filer och förnyar metadata.", - "TaskRefreshLibrary": "Genomsök mediabibliotek", + "TaskCleanLogs": "Rensa Loggkatalog", + "TaskRefreshLibraryDescription": "Scannar ditt mediabibliotek efter nya filer och förnyar metadata.", + "TaskRefreshLibrary": "Scanna Mediabibliotek", "TaskRefreshChapterImagesDescription": "Skapa miniatyrbilder för videor med kapitel.", - "TaskRefreshChapterImages": "Extrahera kapitelbilder", + "TaskRefreshChapterImages": "Extrahera Kapitelbilder", "TaskCleanCacheDescription": "Radera cachade filer som systemet inte längre behöver.", - "TaskCleanCache": "Rensa cachekatalog", + "TaskCleanCache": "Rensa Cachekatalog", "TasksChannelsCategory": "Internetkanaler", "TasksApplicationCategory": "Applikation", "TasksLibraryCategory": "Bibliotek", "TasksMaintenanceCategory": "Underhåll", "TaskRefreshPeople": "Uppdatera Personer", - "TaskCleanActivityLogDescription": "Radera aktivitets logg inlägg som är äldre än definerad ålder.", - "TaskCleanActivityLog": "Rensa Aktivitets Logg", + "TaskCleanActivityLogDescription": "Radera aktivitetslogginlägg äldre än konfigurerad ålder.", + "TaskCleanActivityLog": "Rensa Aktivitetslogg", "Undefined": "odefinierad", "Forced": "Tvingad", "Default": "Standard", - "TaskOptimizeDatabase": "Optimera databasen", - "TaskOptimizeDatabaseDescription": "Komprimerar databasen och trunkerar ledigt utrymme. Prestandan kan förbättras genom att köra denna task efter att du har skannat biblioteket eller gjort andra förändringar som indikerar att databasen har modifierats." + "TaskOptimizeDatabase": "Optimera Databasen", + "TaskOptimizeDatabaseDescription": "Komprimerar databasen och trunkerar ledigt utrymme. Prestandan kan förbättras genom att köra denna task efter att du har skannat biblioteket eller gjort andra förändringar som indikerar att databasen har modifierats.", + "TaskKeyframeExtractorDescription": "Exporterar keyframes från videofiler för att skapa mer exakta HLS-spellistor. Denna rutin kan ta lång tid.", + "TaskKeyframeExtractor": "Keyframe-Extraktor", + "External": "Extern" } diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json index 98d763fcd..dfce6bd25 100644 --- a/Emby.Server.Implementations/Localization/Core/ta.json +++ b/Emby.Server.Implementations/Localization/Core/ta.json @@ -119,5 +119,8 @@ "Forced": "கட்டாயப்படுத்தப்பட்டது", "Default": "இயல்புநிலை", "TaskOptimizeDatabaseDescription": "தரவுத்தளத்தை சுருக்கி, இலவச இடத்தை குறைக்கிறது. நூலகத்தை ஸ்கேன் செய்தபின் அல்லது தரவுத்தள மாற்றங்களைக் குறிக்கும் பிற மாற்றங்களைச் செய்தபின் இந்த பணியை இயக்குவது செயல்திறனை மேம்படுத்தக்கூடும்.", - "TaskOptimizeDatabase": "தரவுத்தளத்தை மேம்படுத்தவும்" + "TaskOptimizeDatabase": "தரவுத்தளத்தை மேம்படுத்தவும்", + "TaskKeyframeExtractorDescription": "மிகவும் துல்லியமான HLS பிளேலிஸ்ட்களை உருவாக்க வீடியோ கோப்புகளிலிருந்து கீஃப்ரேம்களைப் பிரித்தெடுக்கிறது. இந்த பணி நீண்ட காலமாக இருக்கலாம்.", + "TaskKeyframeExtractor": "கீஃப்ரேம் எக்ஸ்ட்ராக்டர்", + "External": "வெளி" } diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index 8fadb88ac..b802db982 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -11,7 +11,7 @@ "Collections": "Koleksiyonlar", "DeviceOfflineWithName": "{0} bağlantısı kesildi", "DeviceOnlineWithName": "{0} bağlı", - "FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu", + "FailedLoginAttemptWithUserName": "{0} adresinden giriş denemesi başarısız oldu", "Favorites": "Favoriler", "Folders": "Klasörler", "Genres": "Türler", @@ -120,5 +120,8 @@ "Default": "Varsayılan", "Forced": "Zorla", "TaskOptimizeDatabaseDescription": "Veritabanını sıkıştırır ve boş alanı keser. Kitaplığı taradıktan sonra veya veritabanında değişiklik anlamına gelen diğer işlemleri yaptıktan sonra bu görevi çalıştırmak performansı artırabilir.", - "TaskOptimizeDatabase": "Veritabanını optimize et" + "TaskOptimizeDatabase": "Veritabanını optimize et", + "TaskKeyframeExtractorDescription": "Daha hassas HLS çalma listeleri oluşturmak için video dosyalarından kareleri çıkarır. Bu görev uzun bir süre çalışabilir.", + "TaskKeyframeExtractor": "Kare Ayırt Edici", + "External": "Harici" } diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json index 1c7d73615..b1ed78b40 100644 --- a/Emby.Server.Implementations/Localization/Core/uk.json +++ b/Emby.Server.Implementations/Localization/Core/uk.json @@ -119,5 +119,8 @@ "Undefined": "Не визначено", "Default": "За замовчуванням", "TaskOptimizeDatabase": "Оптимізувати базу даних", - "TaskOptimizeDatabaseDescription": "Стиснення бази даних та збільшення вільного простору. Виконання цього завдання після сканування бібліотеки або внесення інших змін, які передбачають модифікацію бази даних, може покращити продуктивність." + "TaskOptimizeDatabaseDescription": "Стиснення бази даних та збільшення вільного простору. Виконання цього завдання після сканування бібліотеки або внесення інших змін, які передбачають модифікацію бази даних, може покращити продуктивність.", + "TaskKeyframeExtractorDescription": "Витягує ключові кадри з відеофайлів для створення більш точних списків відтворення HLS. Це завдання може виконуватися протягом тривалого часу.", + "TaskKeyframeExtractor": "Екстрактор ключових кадрів", + "External": "Зовнішній" } diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json index d0e08d8ee..9f71e1690 100644 --- a/Emby.Server.Implementations/Localization/Core/vi.json +++ b/Emby.Server.Implementations/Localization/Core/vi.json @@ -119,5 +119,8 @@ "Forced": "Bắt Buộc", "Default": "Mặc Định", "TaskOptimizeDatabaseDescription": "Thu gọn cơ sở dữ liệu và cắt bớt dung lượng trống. Chạy tác vụ này sau khi quét thư viện hoặc thực hiện các thay đổi khác ngụ ý sửa đổi cơ sở dữ liệu có thể cải thiện hiệu suất.", - "TaskOptimizeDatabase": "Tối ưu hóa cơ sở dữ liệu" + "TaskOptimizeDatabase": "Tối ưu hóa cơ sở dữ liệu", + "TaskKeyframeExtractor": "Trích Xuất Khung Hình", + "TaskKeyframeExtractorDescription": "Trích xuất khung hình chính từ các tệp video để tạo danh sách phát HLS chính xác hơn. Tác vụ này có thể chạy trong một thời gian dài.", + "External": "Bên ngoài" } diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index ac4eb644b..ce616cbbb 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -120,5 +120,8 @@ "Forced": "强制的", "Default": "默认", "TaskOptimizeDatabaseDescription": "压缩数据库并优化可用空间,在扫描库或执行其他数据库修改后运行此任务可能会提高性能。", - "TaskOptimizeDatabase": "优化数据库" + "TaskOptimizeDatabase": "优化数据库", + "TaskKeyframeExtractorDescription": "从视频文件中提取关键帧以创建更准确的HLS播放列表。这项任务可能需要很长时间。", + "TaskKeyframeExtractor": "关键帧提取器", + "External": "外部" } diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 1cc97bc27..ac74da67d 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -103,7 +103,7 @@ "TaskCleanTranscodeDescription": "刪除超過一天的轉碼文件。", "TaskCleanTranscode": "清理轉碼目錄", "TaskUpdatePluginsDescription": "下載並安裝配置為自動更新的插件的更新。", - "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的metadata。", + "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的元數據。", "TaskCleanLogsDescription": "刪除超過{0}天的日誌文件。", "TaskCleanLogs": "清理日誌目錄", "TaskRefreshLibrary": "掃描媒體庫", diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json index 585d81450..601e071b6 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-TW.json +++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json @@ -119,5 +119,8 @@ "Forced": "強制", "Default": "原本", "TaskOptimizeDatabaseDescription": "縮小資料庫並釋放可用空間。在掃描資料庫或進行資料庫相關的更動後使用此功能會增加效能。", - "TaskOptimizeDatabase": "最佳化資料庫" + "TaskOptimizeDatabase": "最佳化資料庫", + "TaskKeyframeExtractorDescription": "將關鍵幀從影片檔案提取出來並建立更精準的HLS播放清單。這可能需要很長時間。", + "TaskKeyframeExtractor": "關鍵幀提取器", + "External": "外部" } diff --git a/Emby.Server.Implementations/Localization/Ratings/au.csv b/Emby.Server.Implementations/Localization/Ratings/au.csv index 940375e26..11f4ed94c 100644 --- a/Emby.Server.Implementations/Localization/Ratings/au.csv +++ b/Emby.Server.Implementations/Localization/Ratings/au.csv @@ -2,7 +2,6 @@ AU-G,1 AU-PG,5 AU-M,6 AU-MA15+,7 -AU-M15+,8 AU-R18+,9 AU-X18+,10 AU-RC,11 diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs index fd3fc31c9..21795c8f8 100644 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ b/Emby.Server.Implementations/Net/SocketFactory.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -63,18 +61,13 @@ namespace Emby.Server.Implementations.Net } /// <inheritdoc /> - public ISocket CreateUdpMulticastSocket(string ipAddress, int multicastTimeToLive, int localPort) + public ISocket CreateUdpMulticastSocket(IPAddress ipAddress, int multicastTimeToLive, int localPort) { if (ipAddress == null) { throw new ArgumentNullException(nameof(ipAddress)); } - if (ipAddress.Length == 0) - { - throw new ArgumentException("ipAddress cannot be an empty string.", nameof(ipAddress)); - } - if (multicastTimeToLive <= 0) { throw new ArgumentException("multicastTimeToLive cannot be zero or less.", nameof(multicastTimeToLive)); @@ -87,14 +80,7 @@ namespace Emby.Server.Implementations.Net var retVal = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - try - { - // not supported on all platforms. throws on ubuntu with .net core 2.0 - retVal.ExclusiveAddressUse = false; - } - catch (SocketException) - { - } + retVal.ExclusiveAddressUse = false; try { @@ -114,7 +100,7 @@ namespace Emby.Server.Implementations.Net var localIp = IPAddress.Any; - retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse(ipAddress), localIp)); + retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(ipAddress, localIp)); retVal.MulticastLoopback = true; return new UdpSocket(retVal, localPort, localIp); diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 02df2fffe..9e7035cb3 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -139,7 +139,9 @@ namespace Emby.Server.Implementations.Playlists { new Share { - UserId = options.UserId.Equals(Guid.Empty) ? null : options.UserId.ToString("N", CultureInfo.InvariantCulture), + UserId = options.UserId.Equals(default) + ? null + : options.UserId.ToString("N", CultureInfo.InvariantCulture), CanEdit = true } } @@ -188,7 +190,7 @@ namespace Emby.Server.Implementations.Playlists public Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId) { - var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId); + var user = userId.Equals(default) ? null : _userManager.GetUserById(userId); return AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false) { diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index a805924dd..45ef36441 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -483,7 +483,7 @@ namespace Emby.Server.Implementations.Plugins var pluginStr = instance.Version.ToString(); bool changed = false; if (string.Equals(manifest.Version, pluginStr, StringComparison.Ordinal) - || manifest.Id != instance.Id) + || !manifest.Id.Equals(instance.Id)) { // If a plugin without a manifest failed to load due to an external issue (eg config), // this updates the manifest to the actual plugin values. diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 6c679ea20..277fdf87d 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -373,7 +373,7 @@ namespace Emby.Server.Implementations.Session info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture); } - if (!info.ItemId.Equals(Guid.Empty) && info.Item == null && libraryItem != null) + if (!info.ItemId.Equals(default) && info.Item == null && libraryItem != null) { var current = session.NowPlayingItem; @@ -424,9 +424,14 @@ namespace Emby.Server.Implementations.Session var nowPlayingQueue = info.NowPlayingQueue; - if (nowPlayingQueue != null) + if (nowPlayingQueue?.Length > 0) { session.NowPlayingQueue = nowPlayingQueue; + + var itemIds = nowPlayingQueue.Select(queue => queue.Id).ToArray(); + session.NowPlayingQueueFullItems = _dtoService.GetBaseItemDtos( + _libraryManager.GetItemList(new InternalItemsQuery { ItemIds = itemIds }), + new DtoOptions(true)); } } @@ -553,22 +558,24 @@ namespace Emby.Server.Implementations.Session { var users = new List<User>(); - if (session.UserId != Guid.Empty) + if (session.UserId.Equals(default)) { - var user = _userManager.GetUserById(session.UserId); - - if (user == null) - { - throw new InvalidOperationException("User not found"); - } + return users; + } - users.Add(user); + var user = _userManager.GetUserById(session.UserId); - users.AddRange(session.AdditionalUsers - .Select(i => _userManager.GetUserById(i.UserId)) - .Where(i => i != null)); + if (user == null) + { + throw new InvalidOperationException("User not found"); } + users.Add(user); + + users.AddRange(session.AdditionalUsers + .Select(i => _userManager.GetUserById(i.UserId)) + .Where(i => i != null)); + return users; } @@ -660,7 +667,7 @@ namespace Emby.Server.Implementations.Session var session = GetSession(info.SessionId); - var libraryItem = info.ItemId == Guid.Empty + var libraryItem = info.ItemId.Equals(default) ? null : GetNowPlayingItem(session, info.ItemId); @@ -755,7 +762,7 @@ namespace Emby.Server.Implementations.Session var session = GetSession(info.SessionId); - var libraryItem = info.ItemId.Equals(Guid.Empty) + var libraryItem = info.ItemId.Equals(default) ? null : GetNowPlayingItem(session, info.ItemId); @@ -892,7 +899,7 @@ namespace Emby.Server.Implementations.Session session.StopAutomaticProgress(); - var libraryItem = info.ItemId.Equals(Guid.Empty) + var libraryItem = info.ItemId.Equals(default) ? null : GetNowPlayingItem(session, info.ItemId); @@ -902,7 +909,7 @@ namespace Emby.Server.Implementations.Session info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture); } - if (!info.ItemId.Equals(Guid.Empty) && info.Item == null && libraryItem != null) + if (!info.ItemId.Equals(default) && info.Item == null && libraryItem != null) { var current = session.NowPlayingItem; @@ -1122,7 +1129,7 @@ namespace Emby.Server.Implementations.Session var session = GetSessionToRemoteControl(sessionId); - var user = session.UserId == Guid.Empty ? null : _userManager.GetUserById(session.UserId); + var user = session.UserId.Equals(default) ? null : _userManager.GetUserById(session.UserId); List<BaseItem> items; @@ -1177,7 +1184,7 @@ namespace Emby.Server.Implementations.Session EnableImages = false }) .Where(i => !i.IsVirtualItem) - .SkipWhile(i => i.Id != episode.Id) + .SkipWhile(i => !i.Id.Equals(episode.Id)) .ToList(); if (episodes.Count > 0) @@ -1191,7 +1198,7 @@ namespace Emby.Server.Implementations.Session { var controllingSession = GetSession(controllingSessionId); AssertCanControl(session, controllingSession); - if (!controllingSession.UserId.Equals(Guid.Empty)) + if (!controllingSession.UserId.Equals(default)) { command.ControllingUserId = controllingSession.UserId; } @@ -1310,7 +1317,7 @@ namespace Emby.Server.Implementations.Session { var controllingSession = GetSession(controllingSessionId); AssertCanControl(session, controllingSession); - if (!controllingSession.UserId.Equals(Guid.Empty)) + if (!controllingSession.UserId.Equals(default)) { command.ControllingUserId = controllingSession.UserId.ToString("N", CultureInfo.InvariantCulture); } @@ -1383,12 +1390,12 @@ namespace Emby.Server.Implementations.Session var session = GetSession(sessionId); - if (session.UserId == userId) + if (session.UserId.Equals(userId)) { throw new ArgumentException("The requested user is already the primary user of the session."); } - if (session.AdditionalUsers.All(i => i.UserId != userId)) + if (session.AdditionalUsers.All(i => !i.UserId.Equals(userId))) { var user = _userManager.GetUserById(userId); @@ -1458,7 +1465,7 @@ namespace Emby.Server.Implementations.Session CheckDisposed(); User user = null; - if (request.UserId != Guid.Empty) + if (!request.UserId.Equals(default)) { user = _userManager.GetUserById(request.UserId); } @@ -1787,7 +1794,7 @@ namespace Emby.Server.Implementations.Session throw new ArgumentNullException(nameof(info)); } - var user = info.UserId == Guid.Empty + var user = info.UserId.Equals(default) ? null : _userManager.GetUserById(info.UserId); diff --git a/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs b/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs new file mode 100644 index 000000000..e39280a10 --- /dev/null +++ b/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs @@ -0,0 +1,50 @@ +using System; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Sorting; +using MediaBrowser.Model.Querying; + +namespace Emby.Server.Implementations.Sorting +{ + /// <summary> + /// Class IndexNumberComparer. + /// </summary> + public class IndexNumberComparer : IBaseItemComparer + { + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + public string Name => ItemSortBy.IndexNumber; + + /// <summary> + /// Compares the specified x. + /// </summary> + /// <param name="x">The x.</param> + /// <param name="y">The y.</param> + /// <returns>System.Int32.</returns> + public int Compare(BaseItem? x, BaseItem? y) + { + if (x == null) + { + throw new ArgumentNullException(nameof(x)); + } + + if (y == null) + { + throw new ArgumentNullException(nameof(y)); + } + + if (!x.IndexNumber.HasValue) + { + return -1; + } + + if (!y.IndexNumber.HasValue) + { + return 1; + } + + return x.IndexNumber.Value.CompareTo(y.IndexNumber.Value); + } + } +} diff --git a/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs b/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs new file mode 100644 index 000000000..ffc4e0cad --- /dev/null +++ b/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs @@ -0,0 +1,50 @@ +using System; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Sorting; +using MediaBrowser.Model.Querying; + +namespace Emby.Server.Implementations.Sorting +{ + /// <summary> + /// Class ParentIndexNumberComparer. + /// </summary> + public class ParentIndexNumberComparer : IBaseItemComparer + { + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + public string Name => ItemSortBy.ParentIndexNumber; + + /// <summary> + /// Compares the specified x. + /// </summary> + /// <param name="x">The x.</param> + /// <param name="y">The y.</param> + /// <returns>System.Int32.</returns> + public int Compare(BaseItem? x, BaseItem? y) + { + if (x == null) + { + throw new ArgumentNullException(nameof(x)); + } + + if (y == null) + { + throw new ArgumentNullException(nameof(y)); + } + + if (!x.ParentIndexNumber.HasValue) + { + return -1; + } + + if (!y.ParentIndexNumber.HasValue) + { + return 1; + } + + return x.ParentIndexNumber.Value.CompareTo(y.ParentIndexNumber.Value); + } + } +} diff --git a/Emby.Server.Implementations/SyncPlay/Group.cs b/Emby.Server.Implementations/SyncPlay/Group.cs index 75cf890e5..52becfec6 100644 --- a/Emby.Server.Implementations/SyncPlay/Group.cs +++ b/Emby.Server.Implementations/SyncPlay/Group.cs @@ -553,7 +553,7 @@ namespace Emby.Server.Implementations.SyncPlay if (playingItemRemoved) { var itemId = PlayQueue.GetPlayingItemId(); - if (!itemId.Equals(Guid.Empty)) + if (!itemId.Equals(default)) { var item = _libraryManager.GetItemById(itemId); RunTimeTicks = item.RunTimeTicks ?? 0; diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index a18af27f3..727b9d4b5 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -126,7 +126,8 @@ namespace Emby.Server.Implementations.TV parentsFolders.ToList()) .Cast<Episode>() .Where(episode => !string.IsNullOrEmpty(episode.SeriesPresentationUniqueKey)) - .Select(GetUniqueSeriesKey); + .Select(GetUniqueSeriesKey) + .ToList(); // Avoid implicitly captured closure var episodes = GetNextUpEpisodes(request, user, items, options); @@ -134,13 +135,21 @@ namespace Emby.Server.Implementations.TV return GetResult(episodes, request); } - public IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IEnumerable<string> seriesKeys, DtoOptions dtoOptions) + public IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IReadOnlyList<string> seriesKeys, DtoOptions dtoOptions) { // Avoid implicitly captured closure var currentUser = user; var allNextUp = seriesKeys - .Select(i => GetNextUp(i, currentUser, dtoOptions)); + .Select(i => GetNextUp(i, currentUser, dtoOptions, false)); + + if (request.EnableRewatching) + { + allNextUp = allNextUp.Concat( + seriesKeys.Select(i => GetNextUp(i, currentUser, dtoOptions, true)) + ) + .OrderByDescending(i => i.Item1); + } // If viewing all next up for all series, remove first episodes // But if that returns empty, keep those first episodes (avoid completely empty view) @@ -186,9 +195,9 @@ namespace Emby.Server.Implementations.TV /// Gets the next up. /// </summary> /// <returns>Task{Episode}.</returns> - private Tuple<DateTime, Func<Episode>> GetNextUp(string seriesKey, User user, DtoOptions dtoOptions) + private Tuple<DateTime, Func<Episode>> GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching) { - var lastWatchedEpisode = _libraryManager.GetItemList(new InternalItemsQuery(user) + var lastQuery = new InternalItemsQuery(user) { AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, @@ -202,23 +211,43 @@ namespace Emby.Server.Implementations.TV Fields = new[] { ItemFields.SortName }, EnableImages = false } - }).Cast<Episode>().FirstOrDefault(); + }; + + if (rewatching) + { + // find last watched by date played, not by newest episode watched + lastQuery.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) }; + } + + var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault(); Func<Episode> getEpisode = () => { - var nextEpisode = _libraryManager.GetItemList(new InternalItemsQuery(user) + var nextQuery = new InternalItemsQuery(user) { AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, IncludeItemTypes = new[] { BaseItemKind.Episode }, OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, Limit = 1, - IsPlayed = false, + IsPlayed = rewatching, IsVirtualItem = false, ParentIndexNumberNotEquals = 0, MinSortName = lastWatchedEpisode?.SortName, DtoOptions = dtoOptions - }).Cast<Episode>().FirstOrDefault(); + }; + + Episode nextEpisode; + if (rewatching) + { + nextQuery.Limit = 2; + // get watched episode after most recently watched + nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().ElementAtOrDefault(1); + } + else + { + nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault(); + } if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons) { @@ -228,7 +257,7 @@ namespace Emby.Server.Implementations.TV SeriesPresentationUniqueKey = seriesKey, ParentIndexNumber = 0, IncludeItemTypes = new[] { BaseItemKind.Episode }, - IsPlayed = false, + IsPlayed = rewatching, IsVirtualItem = false, DtoOptions = dtoOptions }) @@ -251,7 +280,7 @@ namespace Emby.Server.Implementations.TV .Cast<Episode>(); if (lastWatchedEpisode != null) { - sortedConsideredEpisodes = sortedConsideredEpisodes.SkipWhile(episode => episode.Id != lastWatchedEpisode.Id).Skip(1); + sortedConsideredEpisodes = sortedConsideredEpisodes.SkipWhile(episode => !episode.Id.Equals(lastWatchedEpisode.Id)).Skip(1); } nextEpisode = sortedConsideredEpisodes.FirstOrDefault(); diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 5eb4c9ffa..40c386e82 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -227,9 +227,9 @@ namespace Emby.Server.Implementations.Updates availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); } - if (id != default) + if (!id.Equals(default)) { - availablePackages = availablePackages.Where(x => x.Id == id); + availablePackages = availablePackages.Where(x => x.Id.Equals(id)); } if (specificVersion != null) @@ -399,7 +399,7 @@ namespace Emby.Server.Implementations.Updates { lock (_currentInstallationsLock) { - var install = _currentInstallations.Find(x => x.Info.Id == id); + var install = _currentInstallations.Find(x => x.Info.Id.Equals(id)); if (install == default((InstallationInfo, CancellationTokenSource))) { return false; @@ -498,7 +498,7 @@ namespace Emby.Server.Implementations.Updates var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version); var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version); - if (version != null && CompletedInstallations.All(x => x.Id != version.Id)) + if (version != null && CompletedInstallations.All(x => !x.Id.Equals(version.Id))) { yield return version; } diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index b54825775..44796bcc4 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -126,7 +126,7 @@ namespace Jellyfin.Api.Controllers User? user = null; BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); - if (userId.HasValue && !userId.Equals(Guid.Empty)) + if (userId.HasValue && !userId.Equals(default)) { user = _userManager.GetUserById(userId.Value); } @@ -329,7 +329,7 @@ namespace Jellyfin.Api.Controllers User? user = null; BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); - if (userId.HasValue && !userId.Equals(Guid.Empty)) + if (userId.HasValue && !userId.Equals(default)) { user = _userManager.GetUserById(userId.Value); } @@ -467,7 +467,7 @@ namespace Jellyfin.Api.Controllers var item = _libraryManager.GetArtist(name, dtoOptions); - if (userId.HasValue && !userId.Equals(Guid.Empty)) + if (userId.HasValue && !userId.Value.Equals(default)) { var user = _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index 54bd80095..d5b589a3f 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -125,9 +125,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var query = new InternalItemsQuery(user) { @@ -199,9 +199,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var query = new InternalItemsQuery(user) { diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs index 87cb418d9..c8411f44b 100644 --- a/Jellyfin.Api/Controllers/DashboardController.cs +++ b/Jellyfin.Api/Controllers/DashboardController.cs @@ -4,10 +4,12 @@ using System.IO; using System.Linq; using System.Net.Mime; using Jellyfin.Api.Attributes; +using Jellyfin.Api.Constants; using Jellyfin.Api.Models; using MediaBrowser.Common.Plugins; using MediaBrowser.Model.Net; using MediaBrowser.Model.Plugins; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -46,6 +48,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("web/ConfigurationPages")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [Authorize(Policy = Policies.DefaultAuthorization)] public ActionResult<IEnumerable<ConfigurationPageInfo>> GetConfigurationPages( [FromQuery] bool? enableInMainMenu) { diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 0b2604640..27eb22339 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -126,9 +126,11 @@ namespace Jellyfin.Api.Controllers HomeSectionType.SmallLibraryTiles, HomeSectionType.Resume, HomeSectionType.ResumeAudio, + HomeSectionType.ResumeBook, HomeSectionType.LiveTv, HomeSectionType.NextUp, - HomeSectionType.LatestMedia, HomeSectionType.None, + HomeSectionType.LatestMedia, + HomeSectionType.None, }; if (!Guid.TryParse(displayPreferencesId, out var itemId)) @@ -182,7 +184,7 @@ namespace Jellyfin.Api.Controllers var order = int.Parse(key.AsSpan().Slice("homesection".Length)); if (!Enum.TryParse<HomeSectionType>(displayPreferences.CustomPrefs[key], true, out var type)) { - type = order < 7 ? defaults[order] : HomeSectionType.None; + type = order < 8 ? defaults[order] : HomeSectionType.None; } displayPreferences.CustomPrefs.Remove(key); diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index f2fdeeea5..f8e8d975c 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1599,7 +1599,6 @@ namespace Jellyfin.Api.Controllers state.BaseRequest.BreakOnNonKeyFrames = false; } - var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions); var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty; var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath)); @@ -1608,12 +1607,15 @@ namespace Jellyfin.Api.Controllers var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer); var outputTsArg = outputPrefix + "%d" + outputExtension; - var segmentFormat = outputExtension.TrimStart('.'); - if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase)) + var segmentFormat = string.Empty; + var segmentContainer = outputExtension.TrimStart('.'); + var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions, segmentContainer); + + if (string.Equals(segmentContainer, "ts", StringComparison.OrdinalIgnoreCase)) { segmentFormat = "mpegts"; } - else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase)) { var outputFmp4HeaderArg = OperatingSystem.IsWindows() switch { @@ -1627,7 +1629,8 @@ namespace Jellyfin.Api.Controllers } else { - _logger.LogError("Invalid HLS segment container: {SegmentFormat}", segmentFormat); + _logger.LogError("Invalid HLS segment container: {SegmentContainer}, default to mpegts", segmentContainer); + segmentFormat = "mpegts"; } var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128 @@ -1647,7 +1650,7 @@ namespace Jellyfin.Api.Controllers CultureInfo.InvariantCulture, "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9}{10} -hls_segment_filename \"{12}\" -hls_playlist_type {11} -hls_list_size 0 -y \"{13}\"", inputModifier, - _encodingHelper.GetInputArgument(state, _encodingOptions), + _encodingHelper.GetInputArgument(state, _encodingOptions, segmentContainer), threads, mapArgs, GetVideoArguments(state, startNumber, isEventPlaylist), @@ -1944,7 +1947,7 @@ namespace Jellyfin.Api.Controllers return Task.CompletedTask; }); - return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath), false, HttpContext); + return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath)); } private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension) diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index a4f12666d..11808b1b8 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -52,9 +52,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); BaseItem? item = null; if (includeItemTypes.Length != 1 @@ -144,9 +144,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? isSeries, [FromQuery] bool? recursive) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); BaseItem? parentItem = null; if (includeItemTypes.Length == 1 diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index 37e6ae184..e28a50750 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -95,7 +95,9 @@ namespace Jellyfin.Api.Controllers .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); - User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null; + User? user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var parentItem = _libraryManager.GetParentItem(parentId, userId); @@ -157,29 +159,26 @@ namespace Jellyfin.Api.Controllers var dtoOptions = new DtoOptions() .AddClientFields(Request); - Genre item = new Genre(); - if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1) + Genre? item; + if (genreName.Contains(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase)) { - var result = GetItemFromSlugName<Genre>(_libraryManager, genreName, dtoOptions, BaseItemKind.Genre); - - if (result != null) - { - item = result; - } + item = GetItemFromSlugName<Genre>(_libraryManager, genreName, dtoOptions, BaseItemKind.Genre); } else { item = _libraryManager.GetGenre(genreName); } - if (userId.HasValue && !userId.Equals(Guid.Empty)) - { - var user = _userManager.GetUserById(userId.Value); + item ??= new Genre(); - return _dtoService.GetBaseItemDto(item, dtoOptions, user); + if (userId is null || userId.Value.Equals(default)) + { + return _dtoService.GetBaseItemDto(item, dtoOptions); } - return _dtoService.GetBaseItemDto(item, dtoOptions); + var user = _userManager.GetUserById(userId.Value); + + return _dtoService.GetBaseItemDto(item, dtoOptions, user); } private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions, BaseItemKind baseItemKind) diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index 7325dca0a..78634f0bf 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -69,7 +69,7 @@ namespace Jellyfin.Api.Controllers return BadRequest("Invalid segment."); } - return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file), false, HttpContext); + return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)); } /// <summary> @@ -186,7 +186,7 @@ namespace Jellyfin.Api.Controllers return Task.CompletedTask; }); - return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path), false, HttpContext); + return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)); } } } diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index aafffc2a1..05d80ba35 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -570,8 +570,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -654,8 +653,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -738,8 +736,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -822,8 +819,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -906,8 +902,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -990,8 +985,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -1074,8 +1068,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -1158,8 +1151,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -1242,8 +1234,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -1326,8 +1317,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -1410,8 +1400,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -1494,8 +1483,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -1596,7 +1584,6 @@ namespace Jellyfin.Api.Controllers backgroundColor, foregroundLayer, null, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase), info) .ConfigureAwait(false); } @@ -1698,7 +1685,6 @@ namespace Jellyfin.Api.Controllers backgroundColor, foregroundLayer, null, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase), info) .ConfigureAwait(false); } @@ -1784,8 +1770,7 @@ namespace Jellyfin.Api.Controllers return await GetImageResult( options, cacheDuration, - ImmutableDictionary<string, string>.Empty, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + ImmutableDictionary<string, string>.Empty) .ConfigureAwait(false); } @@ -1907,7 +1892,6 @@ namespace Jellyfin.Api.Controllers string? backgroundColor, string? foregroundLayer, BaseItem? item, - bool isHeadRequest, ItemImageInfo? imageInfo = null) { if (percentPlayed.HasValue) @@ -1988,8 +1972,7 @@ namespace Jellyfin.Api.Controllers return await GetImageResult( options, cacheDuration, - responseHeaders, - isHeadRequest).ConfigureAwait(false); + responseHeaders).ConfigureAwait(false); } private ImageFormat[] GetOutputFormats(ImageFormat? format) @@ -2068,8 +2051,7 @@ namespace Jellyfin.Api.Controllers private async Task<ActionResult> GetImageResult( ImageProcessingOptions imageProcessingOptions, TimeSpan? cacheDuration, - IDictionary<string, string> headers, - bool isHeadRequest) + IDictionary<string, string> headers) { var (imagePath, imageContentType, dateImageModified) = await _imageProcessor.ProcessImage(imageProcessingOptions).ConfigureAwait(false); @@ -2120,12 +2102,6 @@ namespace Jellyfin.Api.Controllers } } - // if the request is a head request, return a NoContent result with the same headers as it would with a GET request - if (isHeadRequest) - { - return NoContent(); - } - return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain); } } diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index e9d48b624..9abea5938 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -75,9 +75,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var item = _libraryManager.GetItemById(id); - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -111,9 +111,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var album = _libraryManager.GetItemById(id); - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -147,9 +147,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var playlist = (Playlist)_libraryManager.GetItemById(id); - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -182,9 +182,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -218,9 +218,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var item = _libraryManager.GetItemById(id); - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -254,9 +254,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var item = _libraryManager.GetItemById(id); - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -327,9 +327,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var item = _libraryManager.GetItemById(id); - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs index 49865eb5e..9340737b5 100644 --- a/Jellyfin.Api/Controllers/ItemRefreshController.cs +++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs @@ -15,7 +15,7 @@ namespace Jellyfin.Api.Controllers /// Item Refresh Controller. /// </summary> [Route("Items")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.RequiresElevation)] public class ItemRefreshController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; @@ -53,7 +53,7 @@ namespace Jellyfin.Api.Controllers [Description("Refreshes metadata for an item.")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult Post( + public ActionResult RefreshItem( [FromRoute, Required] Guid itemId, [FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None, [FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None, diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index dc7af0a20..2794a06f3 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -228,7 +228,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true) { - var user = userId == Guid.Empty ? null : _userManager.GetUserById(userId); + var user = userId.Equals(default) ? null : _userManager.GetUserById(userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -799,7 +799,7 @@ namespace Jellyfin.Api.Controllers var ancestorIds = Array.Empty<Guid>(); var excludeFolderIds = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes); - if (parentIdGuid.Equals(Guid.Empty) && excludeFolderIds.Length > 0) + if (parentIdGuid.Equals(default) && excludeFolderIds.Length > 0) { ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true) .Where(i => i is Folder) @@ -812,7 +812,7 @@ namespace Jellyfin.Api.Controllers if (excludeActiveSessions) { excludeItemIds = _sessionManager.Sessions - .Where(s => s.UserId == userId && s.NowPlayingItem != null) + .Where(s => s.UserId.Equals(userId) && s.NowPlayingItem != null) .Select(s => s.NowPlayingItem.Id) .ToArray(); } diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index c65462ab5..4cc17dd0f 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -149,14 +149,14 @@ namespace Jellyfin.Api.Controllers [FromQuery] Guid? userId, [FromQuery] bool inheritFromParent = false) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; - - var item = itemId.Equals(Guid.Empty) - ? (!userId.Equals(Guid.Empty) - ? _libraryManager.GetUserRootFolder() - : _libraryManager.RootFolder) + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + + var item = itemId.Equals(default) + ? (userId is null || userId.Value.Equals(default) + ? _libraryManager.RootFolder + : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); if (item == null) @@ -215,14 +215,14 @@ namespace Jellyfin.Api.Controllers [FromQuery] Guid? userId, [FromQuery] bool inheritFromParent = false) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; - - var item = itemId.Equals(Guid.Empty) - ? (!userId.Equals(Guid.Empty) - ? _libraryManager.GetUserRootFolder() - : _libraryManager.RootFolder) + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + + var item = itemId.Equals(default) + ? (userId is null || userId.Value.Equals(default) + ? _libraryManager.RootFolder + : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); if (item == null) @@ -407,9 +407,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] Guid? userId, [FromQuery] bool? isFavorite) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var counts = new ItemCounts { @@ -449,9 +449,9 @@ namespace Jellyfin.Api.Controllers var baseItemDtos = new List<BaseItemDto>(); - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions().AddClientFields(Request); BaseItem? parent = item.GetParent(); @@ -689,10 +689,10 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? limit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) { - var item = itemId.Equals(Guid.Empty) - ? (!userId.Equals(Guid.Empty) - ? _libraryManager.GetUserRootFolder() - : _libraryManager.RootFolder) + var item = itemId.Equals(default) + ? (userId is null || userId.Value.Equals(default) + ? _libraryManager.RootFolder + : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); if (item is Episode || (item is IItemByName && item is not MusicArtist)) @@ -700,9 +700,9 @@ namespace Jellyfin.Api.Controllers return new QueryResult<BaseItemDto>(); } - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request); diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 484b0a974..05340099b 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -180,9 +180,9 @@ namespace Jellyfin.Api.Controllers dtoOptions, CancellationToken.None); - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var fieldsList = dtoOptions.Fields.ToList(); fieldsList.Remove(ItemFields.CanDelete); @@ -211,10 +211,10 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; - var item = channelId.Equals(Guid.Empty) + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + var item = channelId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(channelId); @@ -382,9 +382,9 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] public ActionResult<QueryResult<BaseItemDto>> GetRecordingFolders([FromQuery] Guid? userId) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var folders = _liveTvManager.GetRecordingFolders(user); var returnArray = _dtoService.GetBaseItemDtos(folders, new DtoOptions(), user); @@ -404,10 +404,10 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; - var item = recordingId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId); + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + var item = recordingId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId); var dtoOptions = new DtoOptions() .AddClientFields(Request); @@ -561,9 +561,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool enableTotalRecordCount = true) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var query = new InternalItemsQuery(user) { @@ -588,7 +588,7 @@ namespace Jellyfin.Api.Controllers GenreIds = genreIds }; - if (librarySeriesId != null && !librarySeriesId.Equals(Guid.Empty)) + if (librarySeriesId.HasValue && !librarySeriesId.Equals(default)) { query.IsSeries = true; @@ -617,7 +617,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms([FromBody] GetProgramsDto body) { - var user = body.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(body.UserId); + var user = body.UserId.Equals(default) ? null : _userManager.GetUserById(body.UserId); var query = new InternalItemsQuery(user) { @@ -642,7 +642,7 @@ namespace Jellyfin.Api.Controllers GenreIds = body.GenreIds }; - if (!body.LibrarySeriesId.Equals(Guid.Empty)) + if (!body.LibrarySeriesId.Equals(default)) { query.IsSeries = true; @@ -700,9 +700,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableUserData, [FromQuery] bool enableTotalRecordCount = true) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var query = new InternalItemsQuery(user) { @@ -738,9 +738,9 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string programId, [FromQuery] Guid? userId) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); return await _liveTvManager.GetProgram(programId, CancellationToken.None, user).ConfigureAwait(false); } diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index b422eb78c..75df18204 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -126,7 +126,7 @@ namespace Jellyfin.Api.Controllers var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); var profile = playbackInfoDto?.DeviceProfile; - _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile); + _logger.LogDebug("GetPostedPlaybackInfo profile: {@Profile}", profile); if (profile == null) { @@ -225,14 +225,6 @@ namespace Jellyfin.Api.Controllers } } - if (info.MediaSources != null) - { - foreach (var mediaSource in info.MediaSources) - { - _mediaInfoHelper.NormalizeMediaSourceContainer(mediaSource, profile!, DlnaProfileType.Video); - } - } - return info; } diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index db72ff2f8..420dd9923 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -68,9 +68,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int categoryLimit = 5, [FromQuery] int itemLimit = 8) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request); diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index c4c03aa4f..0499b2985 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -95,7 +95,9 @@ namespace Jellyfin.Api.Controllers .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); - User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null; + User? user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var parentItem = _libraryManager.GetParentItem(parentId, userId); @@ -156,7 +158,7 @@ namespace Jellyfin.Api.Controllers item = _libraryManager.GetMusicGenre(genreName); } - if (userId.HasValue && !userId.Equals(Guid.Empty)) + if (userId.HasValue && !userId.Value.Equals(default)) { var user = _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index ffc748a6e..be4b9eded 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -82,12 +82,9 @@ namespace Jellyfin.Api.Controllers .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - User? user = null; - - if (userId.HasValue && !userId.Equals(Guid.Empty)) - { - user = _userManager.GetUserById(userId.Value); - } + User? user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite); var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery( @@ -127,7 +124,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - if (userId.HasValue && !userId.Equals(Guid.Empty)) + if (userId.HasValue && !userId.Value.Equals(default)) { var user = _userManager.GetUserById(userId.Value); return _dtoService.GetBaseItemDto(item, dtoOptions, user); diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index c18f1b427..ad85f2fb2 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -181,7 +181,9 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null; + var user = userId.Equals(default) + ? null + : _userManager.GetUserById(userId); var items = playlist.GetManageableItems().ToArray(); diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index b41df1abb..b227dba2d 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -7,7 +7,6 @@ using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; -using Jellyfin.Api.Models.PluginDtos; using Jellyfin.Extensions.Json; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; @@ -44,61 +43,6 @@ namespace Jellyfin.Api.Controllers } /// <summary> - /// Get plugin security info. - /// </summary> - /// <response code="200">Plugin security info returned.</response> - /// <returns>Plugin security info.</returns> - [Obsolete("This endpoint should not be used.")] - [HttpGet("SecurityInfo")] - [ProducesResponseType(StatusCodes.Status200OK)] - public static ActionResult<PluginSecurityInfo> GetPluginSecurityInfo() - { - return new PluginSecurityInfo - { - IsMbSupporter = true, - SupporterKey = "IAmTotallyLegit" - }; - } - - /// <summary> - /// Gets registration status for a feature. - /// </summary> - /// <param name="name">Feature name.</param> - /// <response code="200">Registration status returned.</response> - /// <returns>Mb registration record.</returns> - [Obsolete("This endpoint should not be used.")] - [HttpPost("RegistrationRecords/{name}")] - [ProducesResponseType(StatusCodes.Status200OK)] - public static ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name) - { - return new MBRegistrationRecord - { - IsRegistered = true, - RegChecked = true, - TrialVersion = false, - IsValid = true, - RegError = false - }; - } - - /// <summary> - /// Gets registration status for a feature. - /// </summary> - /// <param name="name">Feature name.</param> - /// <response code="501">Not implemented.</response> - /// <returns>Not Implemented.</returns> - /// <exception cref="NotImplementedException">This endpoint is not implemented.</exception> - [Obsolete("Paid plugins are not supported")] - [HttpGet("Registrations/{name}")] - [ProducesResponseType(StatusCodes.Status501NotImplemented)] - public static ActionResult GetRegistration([FromRoute, Required] string name) - { - // TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins, - // delete all these registration endpoints. They are only kept for compatibility. - throw new NotImplementedException(); - } - - /// <summary> /// Gets a list of currently installed plugins. /// </summary> /// <response code="200">Installed plugins returned.</response> @@ -317,20 +261,5 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - - /// <summary> - /// Updates plugin security info. - /// </summary> - /// <param name="pluginSecurityInfo">Plugin security info.</param> - /// <response code="204">Plugin security info updated.</response> - /// <returns>An <see cref="NoContentResult"/>.</returns> - [Obsolete("This endpoint should not be used.")] - [HttpPost("SecurityInfo")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult UpdatePluginSecurityInfo([FromBody, Required] PluginSecurityInfo pluginSecurityInfo) - { - return NoContent(); - } } } diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 6fcd2ae40..6ffedccbd 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -205,7 +205,7 @@ namespace Jellyfin.Api.Controllers break; } - if (!item.ChannelId.Equals(Guid.Empty)) + if (!item.ChannelId.Equals(default)) { var channel = _libraryManager.GetItemById(item.ChannelId); result.ChannelName = channel?.Name; diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index a6bbd40cc..860bccb9b 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -74,7 +74,7 @@ namespace Jellyfin.Api.Controllers result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)); } - if (controllableByUserId.HasValue && !controllableByUserId.Equals(Guid.Empty)) + if (controllableByUserId.HasValue && !controllableByUserId.Equals(default)) { result = result.Where(i => i.SupportsRemoteControl); @@ -82,12 +82,12 @@ namespace Jellyfin.Api.Controllers if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)) { - result = result.Where(i => i.UserId.Equals(Guid.Empty) || i.ContainsUser(controllableByUserId.Value)); + result = result.Where(i => i.UserId.Equals(default) || i.ContainsUser(controllableByUserId.Value)); } if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl)) { - result = result.Where(i => !i.UserId.Equals(Guid.Empty)); + result = result.Where(i => !i.UserId.Equals(default)); } if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0) diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs index 4422ef32c..053c7baaa 100644 --- a/Jellyfin.Api/Controllers/StudiosController.cs +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -91,7 +91,9 @@ namespace Jellyfin.Api.Controllers .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null; + User? user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var parentItem = _libraryManager.GetParentItem(parentId, userId); @@ -141,7 +143,7 @@ namespace Jellyfin.Api.Controllers var dtoOptions = new DtoOptions().AddClientFields(Request); var item = _libraryManager.GetStudio(name); - if (userId.HasValue && !userId.Equals(Guid.Empty)) + if (userId.HasValue && !userId.Equals(default)) { var user = _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index 73be26bb2..e9c46dcf3 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -63,7 +63,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? limit, [FromQuery] bool enableTotalRecordCount = false) { - var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null; + var user = userId.Equals(default) + ? null + : _userManager.GetUserById(userId); var dtoOptions = new DtoOptions().AddClientFields(Request); var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 9425fe519..179a53fd5 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -68,6 +68,7 @@ namespace Jellyfin.Api.Controllers /// <param name="nextUpDateCutoff">Optional. Starting date of shows to show in Next Up section.</param> /// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param> /// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param> + /// <param name="enableRewatching">Whether to include watched episode in next up results.</param> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns> [HttpGet("NextUp")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -84,7 +85,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableUserData, [FromQuery] DateTime? nextUpDateCutoff, [FromQuery] bool enableTotalRecordCount = true, - [FromQuery] bool disableFirstEpisode = false) + [FromQuery] bool disableFirstEpisode = false, + [FromQuery] bool enableRewatching = false) { var options = new DtoOptions { Fields = fields } .AddClientFields(Request) @@ -100,13 +102,14 @@ namespace Jellyfin.Api.Controllers UserId = userId ?? Guid.Empty, EnableTotalRecordCount = enableTotalRecordCount, DisableFirstEpisode = disableFirstEpisode, - NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue + NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue, + EnableRewatching = enableRewatching }, options); - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var returnItems = _dtoService.GetBaseItemDtos(result.Items, options, user); @@ -142,9 +145,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var minPremiereDate = DateTime.UtcNow.Date.AddDays(-1); @@ -213,9 +216,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableUserData, [FromQuery] string? sortBy) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); List<BaseItem> episodes; @@ -329,9 +332,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); if (_libraryManager.GetItemById(seriesId) is not Series series) { diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index bc9527a0b..6fcafd426 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -16,6 +16,7 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -223,7 +224,7 @@ namespace Jellyfin.Api.Controllers DeInterlace = false, RequireNonAnamorphic = false, EnableMpegtsM2TsMode = false, - TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()), + TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(), Context = EncodingContext.Static, StreamOptions = new Dictionary<string, string>(), EnableAdaptiveBitrateStreaming = true @@ -254,7 +255,7 @@ namespace Jellyfin.Api.Controllers CopyTimestamps = true, StartTimeTicks = startTimeTicks, SubtitleMethod = SubtitleDeliveryMethod.Embed, - TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()), + TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(), Context = EncodingContext.Static }; diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 4263d4fe5..6d15d9185 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -534,7 +534,7 @@ namespace Jellyfin.Api.Controllers public ActionResult<UserDto> GetCurrentUser() { var userId = ClaimHelpers.GetUserId(Request.HttpContext.User); - if (userId == null) + if (userId is null) { return BadRequest(); } diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index 008d2f176..e45f9b58c 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -76,7 +76,7 @@ namespace Jellyfin.Api.Controllers { var user = _userManager.GetUserById(userId); - var item = itemId.Equals(Guid.Empty) + var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); @@ -116,7 +116,7 @@ namespace Jellyfin.Api.Controllers { var user = _userManager.GetUserById(userId); - var item = itemId.Equals(Guid.Empty) + var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); @@ -197,7 +197,7 @@ namespace Jellyfin.Api.Controllers { var user = _userManager.GetUserById(userId); - var item = itemId.Equals(Guid.Empty) + var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); @@ -227,7 +227,7 @@ namespace Jellyfin.Api.Controllers { var user = _userManager.GetUserById(userId); - var item = itemId.Equals(Guid.Empty) + var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); @@ -347,7 +347,7 @@ namespace Jellyfin.Api.Controllers { var user = _userManager.GetUserById(userId); - var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); + var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); // Get the user data for this item var data = _userDataRepository.GetUserData(user, item); @@ -370,7 +370,7 @@ namespace Jellyfin.Api.Controllers { var user = _userManager.GetUserById(userId); - var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); + var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); // Get the user data for this item var data = _userDataRepository.GetUserData(user, item); diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index 04171da8a..5cc8c906f 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; using System.Threading.Tasks; +using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.UserViewDtos; @@ -15,6 +16,7 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -24,6 +26,7 @@ namespace Jellyfin.Api.Controllers /// User views controller. /// </summary> [Route("")] + [Authorize(Policy = Policies.DefaultAuthorization)] public class UserViewsController : BaseJellyfinApiController { private readonly IUserManager _userManager; @@ -65,7 +68,7 @@ namespace Jellyfin.Api.Controllers /// <returns>An <see cref="OkResult"/> containing the user views.</returns> [HttpGet("Users/{userId}/Views")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task<ActionResult<QueryResult<BaseItemDto>>> GetUserViews( + public QueryResult<BaseItemDto> GetUserViews( [FromRoute, Required] Guid userId, [FromQuery] bool? includeExternalContent, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews, @@ -87,12 +90,6 @@ namespace Jellyfin.Api.Controllers query.PresetViews = presetViews; } - var app = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Client ?? string.Empty; - if (app.IndexOf("emby rt", StringComparison.OrdinalIgnoreCase) != -1) - { - query.PresetViews = new[] { CollectionType.Movies, CollectionType.TvShows }; - } - var folders = _userViewManager.GetUserViews(query); var dtoOptions = new DtoOptions().AddClientFields(Request); diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 89b150598..62c05331e 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -109,14 +109,14 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<QueryResult<BaseItemDto>> GetAdditionalPart([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; - - var item = itemId.Equals(Guid.Empty) - ? (!userId.Equals(Guid.Empty) - ? _libraryManager.GetUserRootFolder() - : _libraryManager.RootFolder) + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + + var item = itemId.Equals(default) + ? (userId is null || userId.Value.Equals(default) + ? _libraryManager.RootFolder + : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); var dtoOptions = new DtoOptions(); @@ -221,7 +221,7 @@ namespace Jellyfin.Api.Controllers var alternateVersionsOfPrimary = primaryVersion.LinkedAlternateVersions.ToList(); - foreach (var item in items.Where(i => i.Id != primaryVersion.Id)) + foreach (var item in items.Where(i => !i.Id.Equals(primaryVersion.Id))) { item.SetPrimaryVersionId(primaryVersion.Id.ToString("N", CultureInfo.InvariantCulture)); @@ -465,7 +465,7 @@ namespace Jellyfin.Api.Controllers 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); + return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, httpClient, HttpContext).ConfigureAwait(false); } if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File) @@ -494,9 +494,7 @@ namespace Jellyfin.Api.Controllers return FileStreamResponseHelpers.GetStaticFileResult( state.MediaPath, - contentType, - isHeadRequest, - HttpContext); + contentType); } // Need to start ffmpeg (because media can't be returned directly) diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index bac77d43b..7c02e2550 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -90,16 +90,11 @@ namespace Jellyfin.Api.Controllers .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - User? user = null; + User? user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); - if (userId.HasValue && !userId.Equals(Guid.Empty)) - { - user = _userManager.GetUserById(userId.Value); - } - - IList<BaseItem> items; - var query = new InternalItemsQuery(user) { ExcludeItemTypes = excludeItemTypes, @@ -110,17 +105,18 @@ namespace Jellyfin.Api.Controllers bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes); + IList<BaseItem> items; if (parentItem.IsFolder) { var folder = (Folder)parentItem; - if (!userId.Equals(Guid.Empty)) + if (userId.Equals(default)) { - items = recursive ? folder.GetRecursiveChildren(user, query).ToList() : folder.GetChildren(user, true).Where(Filter).ToList(); + items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToList(); } else { - items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToList(); + items = recursive ? folder.GetRecursiveChildren(user, query).ToList() : folder.GetChildren(user, true).Where(Filter).ToList(); } } else @@ -185,7 +181,7 @@ namespace Jellyfin.Api.Controllers var dtoOptions = new DtoOptions() .AddClientFields(Request); - if (userId.HasValue && !userId.Equals(Guid.Empty)) + if (userId.HasValue && !userId.Value.Equals(default)) { var user = _userManager.GetUserById(userId.Value); return _dtoService.GetBaseItemDto(item, dtoOptions, user); diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs index bec961dad..27497cd59 100644 --- a/Jellyfin.Api/Helpers/AudioHelper.cs +++ b/Jellyfin.Api/Helpers/AudioHelper.cs @@ -138,7 +138,7 @@ namespace Jellyfin.Api.Helpers StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager); var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); - return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, _httpContextAccessor.HttpContext).ConfigureAwait(false); + return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, httpClient, _httpContextAccessor.HttpContext).ConfigureAwait(false); } if (streamingRequest.Static && state.InputProtocol != MediaProtocol.File) @@ -167,9 +167,7 @@ namespace Jellyfin.Api.Helpers return FileStreamResponseHelpers.GetStaticFileResult( state.MediaPath, - contentType, - isHeadRequest, - _httpContextAccessor.HttpContext); + contentType); } // Need to start ffmpeg (because media can't be returned directly) diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs index 6385b62c9..5bdd3fe2e 100644 --- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs +++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs @@ -22,14 +22,12 @@ namespace Jellyfin.Api.Helpers /// Returns a static file from a remote source. /// </summary> /// <param name="state">The current <see cref="StreamState"/>.</param> - /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param> /// <param name="httpClient">The <see cref="HttpClient"/> making the remote request.</param> /// <param name="httpContext">The current http context.</param> /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> /// <returns>A <see cref="Task{ActionResult}"/> containing the API response.</returns> public static async Task<ActionResult> GetStaticRemoteStreamResult( StreamState state, - bool isHeadRequest, HttpClient httpClient, HttpContext httpContext, CancellationToken cancellationToken = default) @@ -45,12 +43,6 @@ namespace Jellyfin.Api.Helpers httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none"; - if (isHeadRequest) - { - httpContext.Response.Headers[HeaderNames.ContentType] = contentType; - return new OkResult(); - } - return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType); } @@ -59,23 +51,11 @@ namespace Jellyfin.Api.Helpers /// </summary> /// <param name="path">The path to the file.</param> /// <param name="contentType">The content type of the file.</param> - /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param> - /// <param name="httpContext">The current http context.</param> /// <returns>An <see cref="ActionResult"/> the file.</returns> public static ActionResult GetStaticFileResult( string path, - string contentType, - bool isHeadRequest, - HttpContext httpContext) + string contentType) { - httpContext.Response.ContentType = contentType; - - // if the request is a head request, return an OkResult (200) with the same headers as it would with a GET request - if (isHeadRequest) - { - return new OkResult(); - } - return new PhysicalFileResult(path, contentType) { EnableRangeProcessing = true }; } diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 3b8dc7e31..31b979836 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -89,9 +89,9 @@ namespace Jellyfin.Api.Helpers string? mediaSourceId = null, string? liveStreamId = null) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var item = _libraryManager.GetItemById(id); var result = new PlaybackInfoResponse(); @@ -191,7 +191,9 @@ namespace Jellyfin.Api.Helpers DeviceId = auth.DeviceId, ItemId = item.Id, Profile = profile, - MaxAudioChannels = maxAudioChannels + MaxAudioChannels = maxAudioChannels, + AllowAudioStreamCopy = allowAudioStreamCopy, + AllowVideoStreamCopy = allowVideoStreamCopy }; if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase)) @@ -208,7 +210,7 @@ namespace Jellyfin.Api.Helpers mediaSource.SupportsDirectPlay = false; } - if (!enableDirectStream) + if (!enableDirectStream || !allowVideoStreamCopy) { mediaSource.SupportsDirectStream = false; } @@ -235,168 +237,79 @@ namespace Jellyfin.Api.Helpers user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)); } - // Beginning of Playback Determination: Attempt DirectPlay first - if (mediaSource.SupportsDirectPlay) - { - if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) - { - mediaSource.SupportsDirectPlay = false; - } - else - { - var supportsDirectStream = mediaSource.SupportsDirectStream; - - // Dummy this up to fool StreamBuilder - mediaSource.SupportsDirectStream = true; - options.MaxBitrate = maxBitrate; + options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress); - if (item is Audio) - { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) - { - options.ForceDirectPlay = true; - } - } - else if (item is Video) - { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) - && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) - && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) - { - options.ForceDirectPlay = true; - } - } + if (!options.ForceDirectStream) + { + // direct-stream http streaming is currently broken + options.EnableDirectStream = false; + } - // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - ? streamBuilder.BuildAudioItem(options) - : streamBuilder.BuildVideoItem(options); + // Beginning of Playback Determination + var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) + ? streamBuilder.BuildAudioItem(options) + : streamBuilder.BuildVideoItem(options); - if (streamInfo == null || !streamInfo.IsDirectStream) - { - mediaSource.SupportsDirectPlay = false; - } + if (streamInfo != null) + { + streamInfo.PlaySessionId = playSessionId; + streamInfo.StartPositionTicks = startTimeTicks; - // Set this back to what it was - mediaSource.SupportsDirectStream = supportsDirectStream; + mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay; + // Players do not handle this being set according to PlayMethod + mediaSource.SupportsDirectStream = options.EnableDirectStream ? streamInfo.PlayMethod == PlayMethod.DirectPlay || streamInfo.PlayMethod == PlayMethod.DirectStream : streamInfo.PlayMethod == PlayMethod.DirectPlay; + mediaSource.SupportsTranscoding = streamInfo.PlayMethod == PlayMethod.DirectStream || mediaSource.TranscodingContainer != null; - if (streamInfo != null) + if (item is Audio) + { + if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) { - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; + mediaSource.SupportsTranscoding = false; } } - } - - if (mediaSource.SupportsDirectStream) - { - if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) + else if (item is Video) { - mediaSource.SupportsDirectStream = false; - } - else - { - options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress); - - if (item is Audio) + if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) + && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) + && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) - { - options.ForceDirectStream = true; - } - } - else if (item is Video) - { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) - && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) - && user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) - { - options.ForceDirectStream = true; - } - } - - // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - ? streamBuilder.BuildAudioItem(options) - : streamBuilder.BuildVideoItem(options); - - if (streamInfo == null || !streamInfo.IsDirectStream) - { - mediaSource.SupportsDirectStream = false; - } - - if (streamInfo != null) - { - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; + mediaSource.SupportsTranscoding = false; } } - } - - if (mediaSource.SupportsTranscoding) - { - options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress); - - // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - ? streamBuilder.BuildAudioItem(options) - : streamBuilder.BuildVideoItem(options); if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) { - if (streamInfo != null) - { - streamInfo.PlaySessionId = playSessionId; - streamInfo.StartPositionTicks = startTimeTicks; - mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); - mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; - mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; - mediaSource.TranscodingContainer = streamInfo.Container; - mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; - - // Do this after the above so that StartPositionTicks is set - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; - } + mediaSource.SupportsDirectPlay = false; + mediaSource.SupportsDirectStream = false; + + mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); + mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; + mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; + mediaSource.TranscodingContainer = streamInfo.Container; + mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; } else { - if (streamInfo != null) + if (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream) { - streamInfo.PlaySessionId = playSessionId; + streamInfo.PlayMethod = PlayMethod.Transcode; + mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); - if (streamInfo.PlayMethod == PlayMethod.Transcode) + if (!allowVideoStreamCopy) { - streamInfo.StartPositionTicks = startTimeTicks; - mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); - - if (!allowVideoStreamCopy) - { - mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; - } - - if (!allowAudioStreamCopy) - { - mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; - } - - mediaSource.TranscodingContainer = streamInfo.Container; - mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; + mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; } if (!allowAudioStreamCopy) { mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; } - - mediaSource.TranscodingContainer = streamInfo.Container; - mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; - - // Do this after the above so that StartPositionTicks is set - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; } } + + // Do this after the above so that StartPositionTicks is set + SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; } foreach (var attachment in mediaSource.MediaAttachments) diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index ed071bcd7..34dab75b8 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -102,7 +102,7 @@ namespace Jellyfin.Api.Helpers }; var auth = await authorizationContext.GetAuthorizationInfo(httpRequest).ConfigureAwait(false); - if (!auth.UserId.Equals(Guid.Empty)) + if (!auth.UserId.Equals(default)) { state.User = userManager.GetUserById(auth.UserId); } @@ -151,7 +151,7 @@ namespace Jellyfin.Api.Helpers ? mediaSources[0] : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.Ordinal)); - if (mediaSource == null && Guid.Parse(streamingRequest.MediaSourceId) == streamingRequest.Id) + if (mediaSource == null && Guid.Parse(streamingRequest.MediaSourceId).Equals(streamingRequest.Id)) { mediaSource = mediaSources[0]; } diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 3526d56c6..da3c1530c 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -13,11 +13,13 @@ using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Data.Enums; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; @@ -42,6 +44,8 @@ namespace Jellyfin.Api.Helpers /// </summary> private static readonly Dictionary<string, SemaphoreSlim> _transcodingLocks = new Dictionary<string, SemaphoreSlim>(); + private readonly IAttachmentExtractor _attachmentExtractor; + private readonly IApplicationPaths _appPaths; private readonly IAuthorizationContext _authorizationContext; private readonly EncodingHelper _encodingHelper; private readonly IFileSystem _fileSystem; @@ -55,6 +59,8 @@ namespace Jellyfin.Api.Helpers /// <summary> /// Initializes a new instance of the <see cref="TranscodingJobHelper"/> class. /// </summary> + /// <param name="attachmentExtractor">Instance of the <see cref="IAttachmentExtractor"/> interface.</param> + /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger{TranscodingJobHelpers}"/> interface.</param> /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> @@ -65,6 +71,8 @@ namespace Jellyfin.Api.Helpers /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param> public TranscodingJobHelper( + IAttachmentExtractor attachmentExtractor, + IApplicationPaths appPaths, ILogger<TranscodingJobHelper> logger, IMediaSourceManager mediaSourceManager, IFileSystem fileSystem, @@ -75,6 +83,8 @@ namespace Jellyfin.Api.Helpers EncodingHelper encodingHelper, ILoggerFactory loggerFactory) { + _attachmentExtractor = attachmentExtractor; + _appPaths = appPaths; _logger = logger; _mediaSourceManager = mediaSourceManager; _fileSystem = fileSystem; @@ -449,9 +459,12 @@ namespace Jellyfin.Api.Helpers var audioCodec = state.ActualOutputAudioCodec; var videoCodec = state.ActualOutputVideoCodec; var hardwareAccelerationTypeString = _serverConfigurationManager.GetEncodingOptions().HardwareAccelerationType; - HardwareEncodingType? hardwareAccelerationType = string.IsNullOrEmpty(hardwareAccelerationTypeString) - ? null - : (HardwareEncodingType)Enum.Parse(typeof(HardwareEncodingType), hardwareAccelerationTypeString, true); + HardwareEncodingType? hardwareAccelerationType = null; + if (!string.IsNullOrEmpty(hardwareAccelerationTypeString) + && Enum.TryParse<HardwareEncodingType>(hardwareAccelerationTypeString, out var parsedHardwareAccelerationType)) + { + hardwareAccelerationType = parsedHardwareAccelerationType; + } _sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo { @@ -467,7 +480,7 @@ namespace Jellyfin.Api.Helpers IsAudioDirect = EncodingHelper.IsCopyCodec(state.OutputAudioCodec), IsVideoDirect = EncodingHelper.IsCopyCodec(state.OutputVideoCodec), HardwareAccelerationType = hardwareAccelerationType, - TranscodeReasons = state.TranscodeReasons + TranscodeReason = state.TranscodeReason }); } } @@ -513,6 +526,22 @@ namespace Jellyfin.Api.Helpers throw new ArgumentException("FFmpeg path not set."); } + // If subtitles get burned in fonts may need to be extracted from the media file + if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) + { + var attachmentPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id); + await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false); + + if (state.SubtitleStream.IsExternal && string.Equals(Path.GetExtension(state.SubtitleStream.Path), ".mks", StringComparison.OrdinalIgnoreCase)) + { + string subtitlePath = state.SubtitleStream.Path; + string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal)); + string subtitleId = subtitlePath.GetMD5().ToString("N", CultureInfo.InvariantCulture); + + await _attachmentExtractor.ExtractAllAttachmentsExternal(subtitlePathArgument, subtitleId, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false); + } + } + var process = new Process { StartInfo = new ProcessStartInfo @@ -753,6 +782,8 @@ namespace Jellyfin.Api.Helpers job.HasExited = true; job.ExitCode = process.ExitCode; + ReportTranscodingProgress(job, state, null, null, null, null, null); + _logger.LogDebug("Disposing stream resources"); state.Dispose(); diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index c5b240e92..2b0436e34 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -17,10 +17,10 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.2" /> + <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.3" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" /> - <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.2.3" /> + <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.3.0" /> </ItemGroup> <ItemGroup> @@ -36,7 +36,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs b/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs deleted file mode 100644 index 7f1255f4b..000000000 --- a/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; - -namespace Jellyfin.Api.Models.PluginDtos -{ - /// <summary> - /// MB Registration Record. - /// </summary> - public class MBRegistrationRecord - { - /// <summary> - /// Gets or sets expiration date. - /// </summary> - public DateTime ExpirationDate { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether is registered. - /// </summary> - public bool IsRegistered { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether reg checked. - /// </summary> - public bool RegChecked { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether reg error. - /// </summary> - public bool RegError { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether trial version. - /// </summary> - public bool TrialVersion { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether is valid. - /// </summary> - public bool IsValid { get; set; } - } -} diff --git a/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs b/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs deleted file mode 100644 index a90398425..000000000 --- a/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Jellyfin.Api.Models.PluginDtos -{ - /// <summary> - /// Plugin security info. - /// </summary> - public class PluginSecurityInfo - { - /// <summary> - /// Gets or sets the supporter key. - /// </summary> - public string? SupporterKey { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether is mb supporter. - /// </summary> - public bool IsMbSupporter { get; set; } - } -} diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index c35778065..d39c75e36 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -34,7 +34,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index 44631327e..2188c0d99 100644 --- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -35,7 +35,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj index ef8ef700f..0edf4adcc 100644 --- a/Jellyfin.Networking/Jellyfin.Networking.csproj +++ b/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -16,7 +16,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs index 4447b212d..592c53fe5 100644 --- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -56,7 +56,7 @@ namespace Jellyfin.Server.Implementations.Activity if (query.HasUserId.HasValue) { - entries = entries.Where(entry => entry.UserId != Guid.Empty == query.HasUserId.Value ); + entries = entries.Where(entry => (!entry.UserId.Equals(default)) == query.HasUserId.Value); } return new QueryResult<ActivityLogEntry>( diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index b5fc96079..3203bed18 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -120,7 +120,7 @@ namespace Jellyfin.Server.Implementations.Devices if (query.UserId.HasValue) { - devices = devices.Where(device => device.UserId == query.UserId.Value); + devices = devices.Where(device => device.UserId.Equals(query.UserId.Value)); } if (query.DeviceId != null) diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index b7dab82af..e68b2ed7f 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -17,7 +17,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> @@ -27,13 +27,13 @@ <ItemGroup> <PackageReference Include="System.Linq.Async" Version="6.0.1" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.2" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.2" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.2"> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.3" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.3" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.3"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.2"> + <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.3"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs index c89e3c74d..f5d38db20 100644 --- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs +++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs @@ -32,7 +32,7 @@ namespace Jellyfin.Server.Implementations.Users var prefs = _dbContext.DisplayPreferences .Include(pref => pref.HomeSections) .FirstOrDefault(pref => - pref.UserId == userId && string.Equals(pref.Client, client) && pref.ItemId == itemId); + pref.UserId.Equals(userId) && string.Equals(pref.Client, client) && pref.ItemId.Equals(itemId)); if (prefs == null) { @@ -47,7 +47,7 @@ namespace Jellyfin.Server.Implementations.Users public ItemDisplayPreferences GetItemDisplayPreferences(Guid userId, Guid itemId, string client) { var prefs = _dbContext.ItemDisplayPreferences - .FirstOrDefault(pref => pref.UserId == userId && pref.ItemId == itemId && string.Equals(pref.Client, client)); + .FirstOrDefault(pref => pref.UserId.Equals(userId) && pref.ItemId.Equals(itemId) && string.Equals(pref.Client, client)); if (prefs == null) { @@ -63,7 +63,7 @@ namespace Jellyfin.Server.Implementations.Users { return _dbContext.ItemDisplayPreferences .AsQueryable() - .Where(prefs => prefs.UserId == userId && prefs.ItemId != Guid.Empty && string.Equals(prefs.Client, client)) + .Where(prefs => prefs.UserId.Equals(userId) && !prefs.ItemId.Equals(default) && string.Equals(prefs.Client, client)) .ToList(); } @@ -72,8 +72,8 @@ namespace Jellyfin.Server.Implementations.Users { return _dbContext.CustomItemDisplayPreferences .AsQueryable() - .Where(prefs => prefs.UserId == userId - && prefs.ItemId == itemId + .Where(prefs => prefs.UserId.Equals(userId) + && prefs.ItemId.Equals(itemId) && string.Equals(prefs.Client, client)) .ToDictionary(prefs => prefs.Key, prefs => prefs.Value); } @@ -83,8 +83,8 @@ namespace Jellyfin.Server.Implementations.Users { var existingPrefs = _dbContext.CustomItemDisplayPreferences .AsQueryable() - .Where(prefs => prefs.UserId == userId - && prefs.ItemId == itemId + .Where(prefs => prefs.UserId.Equals(userId) + && prefs.ItemId.Equals(itemId) && string.Equals(prefs.Client, client)); _dbContext.CustomItemDisplayPreferences.RemoveRange(existingPrefs); diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index c41b343c7..2100fa6d5 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -107,7 +107,7 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public User? GetUserById(Guid id) { - if (id == Guid.Empty) + if (id.Equals(default)) { throw new ArgumentException("Guid can't be empty", nameof(id)); } @@ -146,8 +146,7 @@ namespace Jellyfin.Server.Implementations.Users if (await dbContext.Users .AsQueryable() - .Where(u => u.Username == newName && u.Id != user.Id) - .AnyAsync() + .AnyAsync(u => u.Username == newName && !u.Id.Equals(user.Id)) .ConfigureAwait(false)) { throw new ArgumentException(string.Format( @@ -597,7 +596,7 @@ namespace Jellyfin.Server.Implementations.Users .Include(u => u.Preferences) .Include(u => u.AccessSchedules) .Include(u => u.ProfileImage) - .FirstOrDefault(u => u.Id == userId) + .FirstOrDefault(u => u.Id.Equals(userId)) ?? throw new ArgumentException("No user exists with given Id!"); user.SubtitleMode = config.SubtitleMode; @@ -631,7 +630,7 @@ namespace Jellyfin.Server.Implementations.Users .Include(u => u.Preferences) .Include(u => u.AccessSchedules) .Include(u => u.ProfileImage) - .FirstOrDefault(u => u.Id == userId) + .FirstOrDefault(u => u.Id.Equals(userId)) ?? throw new ArgumentException("No user exists with given Id!"); // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0" diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index fa98fda69..3df8481fd 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -29,6 +29,7 @@ using Jellyfin.Server.Filters; using Jellyfin.Server.Formatters; using MediaBrowser.Common.Net; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; @@ -428,6 +429,17 @@ namespace Jellyfin.Server.Extensions Nullable = true } }); + + // Manually describe Flags enum. + options.MapType<TranscodeReason>(() => + new OpenApiSchema + { + Type = "string", + Enum = Enum.GetNames<TranscodeReason>() + .Select(e => new OpenApiString(e)) + .Cast<IOpenApiAny>() + .ToArray() + }); } } } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 6743a24aa..fb021f3af 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -29,7 +29,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> @@ -37,10 +37,10 @@ <PackageReference Include="CommandLineParser" Version="2.8.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" /> - <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.2" /> - <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.2" /> - <PackageReference Include="prometheus-net" Version="5.0.2" /> - <PackageReference Include="prometheus-net.AspNetCore" Version="5.0.2" /> + <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.3" /> + <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.3" /> + <PackageReference Include="prometheus-net" Version="6.0.0" /> + <PackageReference Include="prometheus-net.AspNetCore" Version="6.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" /> <PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" /> <PackageReference Include="Serilog.Settings.Configuration" Version="3.3.0" /> diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs index 21f153623..afd7aee5d 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs @@ -58,13 +58,18 @@ namespace Jellyfin.Server.Migrations.Routines foreach (var row in authenticatedDevices) { + var dateCreatedStr = row[9].ToString(); + _ = DateTime.TryParse(dateCreatedStr, out var dateCreated); + var dateLastActivityStr = row[10].ToString(); + _ = DateTime.TryParse(dateLastActivityStr, out var dateLastActivity); + if (row[6].IsDbNull()) { dbContext.ApiKeys.Add(new ApiKey(row[3].ToString()) { AccessToken = row[1].ToString(), - DateCreated = row[9].ToDateTime(), - DateLastActivity = row[10].ToDateTime() + DateCreated = dateCreated, + DateLastActivity = dateLastActivity }); } else @@ -78,8 +83,8 @@ namespace Jellyfin.Server.Migrations.Routines { AccessToken = row[1].ToString(), IsActive = row[8].ToBool(), - DateCreated = row[9].ToDateTime(), - DateLastActivity = row[10].ToDateTime() + DateCreated = dateCreated, + DateLastActivity = dateLastActivity }); } } diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index b61e104ce..35231a792 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -54,7 +54,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs index 85a99d62c..94418683b 100644 --- a/MediaBrowser.Controller/Channels/Channel.cs +++ b/MediaBrowser.Controller/Channels/Channel.cs @@ -77,11 +77,6 @@ namespace MediaBrowser.Controller.Channels return false; } - protected override bool IsAllowTagFilterEnforced() - { - return false; - } - internal static bool IsChannelVisible(BaseItem channelItem, User user) { var channel = ChannelManager.GetChannel(channelItem.ChannelId.ToString(string.Empty)); diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs index 9589f5245..77a857b78 100644 --- a/MediaBrowser.Controller/Entities/AggregateFolder.cs +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -187,14 +187,14 @@ namespace MediaBrowser.Controller.Entities /// <exception cref="ArgumentNullException">The id is empty.</exception> public BaseItem FindVirtualChild(Guid id) { - if (id.Equals(Guid.Empty)) + if (id.Equals(default)) { throw new ArgumentNullException(nameof(id)); } foreach (var child in _virtualChildren) { - if (child.Id == id) + if (child.Id.Equals(id)) { return child; } diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 11b95b94b..0f2d7e62d 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.Entities.Audio public class MusicArtist : Folder, IItemByName, IHasMusicGenres, IHasDualAccess, IHasLookupInfo<ArtistInfo> { [JsonIgnore] - public bool IsAccessedByName => ParentId.Equals(Guid.Empty); + public bool IsAccessedByName => ParentId.Equals(default); [JsonIgnore] public override bool IsFolder => !IsAccessedByName; diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 0f62e8e1e..2bb966d2c 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -231,7 +231,7 @@ namespace MediaBrowser.Controller.Entities { get { - if (!ChannelId.Equals(Guid.Empty)) + if (!ChannelId.Equals(default)) { return SourceType.Channel; } @@ -521,7 +521,7 @@ namespace MediaBrowser.Controller.Entities get { var id = DisplayParentId; - if (id.Equals(Guid.Empty)) + if (id.Equals(default)) { return null; } @@ -737,7 +737,7 @@ namespace MediaBrowser.Controller.Entities public virtual bool StopRefreshIfLocalMetadataFound => true; [JsonIgnore] - protected virtual bool SupportsOwnedItems => !ParentId.Equals(Guid.Empty) && IsFileProtocol; + protected virtual bool SupportsOwnedItems => !ParentId.Equals(default) && IsFileProtocol; [JsonIgnore] public virtual bool SupportsPeople => false; @@ -848,7 +848,7 @@ namespace MediaBrowser.Controller.Entities public BaseItem GetOwner() { var ownerId = OwnerId; - return ownerId.Equals(Guid.Empty) ? null : LibraryManager.GetItemById(ownerId); + return ownerId.Equals(default) ? null : LibraryManager.GetItemById(ownerId); } public bool CanDelete(User user, List<Folder> allCollectionFolders) @@ -878,16 +878,13 @@ namespace MediaBrowser.Controller.Entities return CanDownload() && IsAuthorizedToDownload(user); } - /// <summary> - /// Returns a <see cref="string" /> that represents this instance. - /// </summary> - /// <returns>A <see cref="string" /> that represents this instance.</returns> + /// <inheritdoc /> public override string ToString() { return Name; } - public string GetInternalMetadataPath() + public virtual string GetInternalMetadataPath() { var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath; @@ -984,12 +981,12 @@ namespace MediaBrowser.Controller.Entities public BaseItem GetParent() { var parentId = ParentId; - if (!parentId.Equals(Guid.Empty)) + if (parentId.Equals(default)) { - return LibraryManager.GetItemById(parentId); + return null; } - return null; + return LibraryManager.GetItemById(parentId); } public IEnumerable<BaseItem> GetParents() @@ -1397,7 +1394,7 @@ namespace MediaBrowser.Controller.Entities var tasks = extras.Select(i => { var subOptions = new MetadataRefreshOptions(options); - if (i.OwnerId != ownerId || i.ParentId != Guid.Empty) + if (!i.OwnerId.Equals(ownerId) || !i.ParentId.Equals(default)) { i.OwnerId = ownerId; i.ParentId = Guid.Empty; @@ -1595,23 +1592,6 @@ namespace MediaBrowser.Controller.Entities return value.Value <= maxAllowedRating.Value; } - public int? GetParentalRatingValue() - { - var rating = CustomRating; - - if (string.IsNullOrEmpty(rating)) - { - rating = OfficialRating; - } - - if (string.IsNullOrEmpty(rating)) - { - return null; - } - - return LocalizationManager.GetRatingLevel(rating); - } - public int? GetInheritedParentalRatingValue() { var rating = CustomRatingForComparison; @@ -1652,11 +1632,6 @@ namespace MediaBrowser.Controller.Entities return true; } - protected virtual bool IsAllowTagFilterEnforced() - { - return true; - } - public virtual UnratedItem GetBlockUnratedType() { if (SourceType == SourceType.Channel) @@ -1736,7 +1711,7 @@ namespace MediaBrowser.Controller.Entities // First get using the cached Id if (info.ItemId.HasValue) { - if (info.ItemId.Value.Equals(Guid.Empty)) + if (info.ItemId.Value.Equals(default)) { return null; } @@ -2657,7 +2632,7 @@ namespace MediaBrowser.Controller.Entities } /// <inheritdoc /> - public bool Equals(BaseItem other) => Id == other?.Id; + public bool Equals(BaseItem other) => other is not null && other.Id.Equals(Id); /// <inheritdoc /> public override int GetHashCode() => HashCode.Combine(Id); diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 4d9aac6f9..b6983b73e 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -189,21 +189,6 @@ namespace MediaBrowser.Controller.Entities return baseResult; } - protected override bool IsAllowTagFilterEnforced() - { - if (this is ICollectionFolder) - { - return false; - } - - if (this is UserView) - { - return false; - } - - return true; - } - /// <summary> /// Adds the child. /// </summary> @@ -213,7 +198,7 @@ namespace MediaBrowser.Controller.Entities { item.SetParent(this); - if (item.Id.Equals(Guid.Empty)) + if (item.Id.Equals(default)) { item.Id = LibraryManager.GetNewItemId(item.Path, item.GetType()); } @@ -730,7 +715,9 @@ namespace MediaBrowser.Controller.Entities return PostFilterAndSort(items, query, true); } - if (this is not UserRootFolder && this is not AggregateFolder && query.ParentId == Guid.Empty) + if (this is not UserRootFolder + && this is not AggregateFolder + && query.ParentId.Equals(default)) { query.Parent = this; } @@ -848,6 +835,18 @@ namespace MediaBrowser.Controller.Entities return true; } + if (query.HasThemeSong.HasValue) + { + Logger.LogDebug("Query requires post-filtering due to HasThemeSong"); + return true; + } + + if (query.HasThemeVideo.HasValue) + { + Logger.LogDebug("Query requires post-filtering due to HasThemeVideo"); + return true; + } + // Filter by VideoType if (query.VideoTypes.Length > 0) { @@ -1492,7 +1491,7 @@ namespace MediaBrowser.Controller.Entities { if (i.ItemId.HasValue) { - if (i.ItemId.Value == itemId) + if (i.ItemId.Value.Equals(itemId)) { return true; } @@ -1502,7 +1501,7 @@ namespace MediaBrowser.Controller.Entities var child = GetLinkedChild(i); - if (child != null && child.Id == itemId) + if (child != null && child.Id.Equals(itemId)) { return true; } diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index c8a0e21eb..15b721fe6 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -74,12 +74,12 @@ namespace MediaBrowser.Controller.Entities.TV get { var seriesId = SeriesId; - if (seriesId.Equals(Guid.Empty)) + if (seriesId.Equals(default)) { seriesId = FindSeriesId(); } - return !seriesId.Equals(Guid.Empty) ? (LibraryManager.GetItemById(seriesId) as Series) : null; + return seriesId.Equals(default) ? null : (LibraryManager.GetItemById(seriesId) as Series); } } @@ -89,12 +89,12 @@ namespace MediaBrowser.Controller.Entities.TV get { var seasonId = SeasonId; - if (seasonId.Equals(Guid.Empty)) + if (seasonId.Equals(default)) { seasonId = FindSeasonId(); } - return !seasonId.Equals(Guid.Empty) ? (LibraryManager.GetItemById(seasonId) as Season) : null; + return seasonId.Equals(default) ? null : (LibraryManager.GetItemById(seasonId) as Season); } } @@ -271,7 +271,7 @@ namespace MediaBrowser.Controller.Entities.TV var seasonId = SeasonId; - if (!seasonId.Equals(Guid.Empty) && !list.Contains(seasonId)) + if (!seasonId.Equals(default) && !list.Contains(seasonId)) { list.Add(seasonId); } diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 926c7b045..bd8df2fac 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -48,12 +48,12 @@ namespace MediaBrowser.Controller.Entities.TV get { var seriesId = SeriesId; - if (seriesId == Guid.Empty) + if (seriesId.Equals(default)) { seriesId = FindSeriesId(); } - return seriesId == Guid.Empty ? null : (LibraryManager.GetItemById(seriesId) as Series); + return seriesId.Equals(default) ? null : (LibraryManager.GetItemById(seriesId) as Series); } } diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index 5c9be7337..47432ee93 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -69,11 +69,11 @@ namespace MediaBrowser.Controller.Entities /// <inheritdoc /> public override IEnumerable<Guid> GetIdsForAncestorQuery() { - if (!DisplayParentId.Equals(Guid.Empty)) + if (!DisplayParentId.Equals(default)) { yield return DisplayParentId; } - else if (!ParentId.Equals(Guid.Empty)) + else if (!ParentId.Equals(default)) { yield return ParentId; } @@ -94,11 +94,11 @@ namespace MediaBrowser.Controller.Entities { var parent = this as Folder; - if (!DisplayParentId.Equals(Guid.Empty)) + if (!DisplayParentId.Equals(default)) { parent = LibraryManager.GetItemById(DisplayParentId) as Folder ?? parent; } - else if (!ParentId.Equals(Guid.Empty)) + else if (!ParentId.Equals(default)) { parent = LibraryManager.GetItemById(ParentId) as Folder ?? parent; } diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 279206da4..2996104e7 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -988,7 +988,7 @@ namespace MediaBrowser.Controller.Entities public static IEnumerable<BaseItem> FilterForAdjacency(List<BaseItem> list, string adjacentToId) { var adjacentToIdGuid = new Guid(adjacentToId); - var adjacentToItem = list.FirstOrDefault(i => i.Id == adjacentToIdGuid); + var adjacentToItem = list.FirstOrDefault(i => i.Id.Equals(adjacentToIdGuid)); var index = list.IndexOf(adjacentToItem); @@ -1005,7 +1005,7 @@ namespace MediaBrowser.Controller.Entities nextId = list[index + 1].Id; } - return list.Where(i => i.Id == previousId || i.Id == nextId || i.Id == adjacentToIdGuid); + return list.Where(i => i.Id.Equals(previousId) || i.Id.Equals(nextId) || i.Id.Equals(adjacentToIdGuid)); } } } diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 5ab7808c3..5de2e0f50 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -455,7 +455,7 @@ namespace MediaBrowser.Controller.Entities foreach (var child in LinkedAlternateVersions) { // Reset the cached value - if (child.ItemId.HasValue && child.ItemId.Value.Equals(Guid.Empty)) + if (child.ItemId.HasValue && child.ItemId.Value.Equals(default)) { child.ItemId = null; } diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index e76a478a5..6164e51cd 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -57,7 +57,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index cbc466515..633ba2d76 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -12,6 +12,7 @@ using System.Text.RegularExpressions; using System.Threading; using Jellyfin.Data.Enums; using Jellyfin.Extensions; +using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; @@ -28,7 +29,7 @@ namespace MediaBrowser.Controller.MediaEncoding private const string VideotoolboxAlias = "vt"; private const string OpenclAlias = "ocl"; private const string CudaAlias = "cu"; - + private readonly IApplicationPaths _appPaths; private readonly IMediaEncoder _mediaEncoder; private readonly ISubtitleEncoder _subtitleEncoder; @@ -51,9 +52,11 @@ namespace MediaBrowser.Controller.MediaEncoding }; public EncodingHelper( + IApplicationPaths appPaths, IMediaEncoder mediaEncoder, ISubtitleEncoder subtitleEncoder) { + _appPaths = appPaths; _mediaEncoder = mediaEncoder; _subtitleEncoder = subtitleEncoder; } @@ -81,7 +84,6 @@ namespace MediaBrowser.Controller.MediaEncoding { "vaapi", hwEncoder + "_vaapi" }, { "videotoolbox", hwEncoder + "_videotoolbox" }, { "v4l2m2m", hwEncoder + "_v4l2m2m" }, - { "omx", hwEncoder + "_omx" }, }; if (!string.IsNullOrEmpty(hwType) @@ -578,13 +580,13 @@ namespace MediaBrowser.Controller.MediaEncoding options); } - private string GetVaapiDeviceArgs(string renderNodePath, string kernelDriver, string driver, string alias) + private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string alias) { alias ??= VaapiAlias; renderNodePath = renderNodePath ?? "/dev/dri/renderD128"; - var options = string.IsNullOrEmpty(kernelDriver) || string.IsNullOrEmpty(driver) + var options = string.IsNullOrEmpty(driver) ? renderNodePath - : ",kernel_driver=" + kernelDriver + ",driver=" + driver; + : ",driver=" + driver + (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver); return string.Format( CultureInfo.InvariantCulture, @@ -599,7 +601,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (OperatingSystem.IsLinux()) { // derive qsv from vaapi device - return GetVaapiDeviceArgs(null, "i915", "iHD", VaapiAlias) + arg + "@" + VaapiAlias; + return GetVaapiDeviceArgs(null, "iHD", "i915", VaapiAlias) + arg + "@" + VaapiAlias; } if (OperatingSystem.IsWindows()) @@ -688,7 +690,19 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Empty; } - args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, VaapiAlias)); + if (_mediaEncoder.IsVaapiDeviceInteliHD) + { + args.Append(GetVaapiDeviceArgs(null, "iHD", null, VaapiAlias)); + } + else if (_mediaEncoder.IsVaapiDeviceInteli965) + { + args.Append(GetVaapiDeviceArgs(null, "i965", null, VaapiAlias)); + } + else + { + args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, VaapiAlias)); + } + var filterDevArgs = GetFilterHwDeviceArgs(VaapiAlias); if (isHwTonemapAvailable && IsOpenclFullSupported()) @@ -768,10 +782,6 @@ namespace MediaBrowser.Controller.MediaEncoding args.Append(GetCudaDeviceArgs(0, CudaAlias)) .Append(GetFilterHwDeviceArgs(CudaAlias)); - - // workaround for "No decoder surfaces left" error, - // but will increase vram usage. https://trac.ffmpeg.org/ticket/7562 - args.Append(" -extra_hw_frames 3"); } else if (string.Equals(optHwaccelType, "amf", StringComparison.OrdinalIgnoreCase)) { @@ -832,8 +842,9 @@ namespace MediaBrowser.Controller.MediaEncoding /// </summary> /// <param name="state">Encoding state.</param> /// <param name="options">Encoding options.</param> + /// <param name="segmentContainer">Segment Container.</param> /// <returns>Input arguments.</returns> - public string GetInputArgument(EncodingJobInfo state, EncodingOptions options) + public string GetInputArgument(EncodingJobInfo state, EncodingOptions options, string segmentContainer) { var arg = new StringBuilder(); var inputVidHwaccelArgs = GetInputVideoHwaccelArgs(state, options); @@ -870,7 +881,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // Also seek the external subtitles stream. - var seekSubParam = GetFastSeekCommandLineParameter(state, options); + var seekSubParam = GetFastSeekCommandLineParameter(state, options, segmentContainer); if (!string.IsNullOrEmpty(seekSubParam)) { arg.Append(' ').Append(seekSubParam); @@ -887,7 +898,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.AudioStream != null && state.AudioStream.IsExternal) { // Also seek the external audio stream. - var seekAudioParam = GetFastSeekCommandLineParameter(state, options); + var seekAudioParam = GetFastSeekCommandLineParameter(state, options, segmentContainer); if (!string.IsNullOrEmpty(seekAudioParam)) { arg.Append(' ').Append(seekAudioParam); @@ -1081,6 +1092,12 @@ namespace MediaBrowser.Controller.MediaEncoding var alphaParam = enableAlpha ? ":alpha=1" : string.Empty; var sub2videoParam = enableSub2video ? ":sub2video=1" : string.Empty; + var fontPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id); + var fontParam = string.Format( + CultureInfo.InvariantCulture, + ":fontsdir='{0}'", + _mediaEncoder.EscapeSubtitleFilterPath(fontPath)); + // TODO // var fallbackFontPath = Path.Combine(_appPaths.ProgramDataPath, "fonts", "DroidSansFallback.ttf"); // string fallbackFontParam = string.Empty; @@ -1121,11 +1138,12 @@ namespace MediaBrowser.Controller.MediaEncoding // TODO: Perhaps also use original_size=1920x800 ?? return string.Format( CultureInfo.InvariantCulture, - "subtitles=f='{0}'{1}{2}{3}{4}", + "subtitles=f='{0}'{1}{2}{3}{4}{5}", _mediaEncoder.EscapeSubtitleFilterPath(subtitlePath), charsetParam, alphaParam, sub2videoParam, + fontParam, // fallbackFontParam, setPtsParam); } @@ -1134,11 +1152,12 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Format( CultureInfo.InvariantCulture, - "subtitles='{0}:si={1}{2}{3}'{4}", + "subtitles=f='{0}':si={1}{2}{3}{4}{5}", _mediaEncoder.EscapeSubtitleFilterPath(mediaPath), state.InternalSubtitleStreamOffset.ToString(CultureInfo.InvariantCulture), alphaParam, sub2videoParam, + fontParam, // fallbackFontParam, setPtsParam); } @@ -1282,11 +1301,6 @@ namespace MediaBrowser.Controller.MediaEncoding param += " -low_power 1"; } - if (string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) - { - param += " -pix_fmt nv21"; - } - var isVc1 = string.Equals(state.VideoStream?.Codec, "vc1", StringComparison.OrdinalIgnoreCase); var isLibX265 = string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase); @@ -1344,29 +1358,37 @@ namespace MediaBrowser.Controller.MediaEncoding switch (encodingOptions.EncoderPreset) { case "veryslow": - - param += " -preset slow"; // lossless is only supported on maxwell and newer(2014+) + param += " -preset p7"; break; case "slow": + param += " -preset p6"; + break; + case "slower": - param += " -preset slow"; + param += " -preset p5"; break; case "medium": - param += " -preset medium"; + param += " -preset p4"; break; case "fast": + param += " -preset p3"; + break; + case "faster": + param += " -preset p2"; + break; + case "veryfast": case "superfast": case "ultrafast": - param += " -preset fast"; + param += " -preset p1"; break; default: - param += " -preset default"; + param += " -preset p4"; break; } } @@ -1572,10 +1594,8 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(profile)) { - if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) - && !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) { - // not supported by h264_omx param += " -profile:v:0 " + profile; } } @@ -1614,8 +1634,7 @@ namespace MediaBrowser.Controller.MediaEncoding // NVENC cannot adjust the given level, just throw an error. // level option may cause corrupted frames on AMD VAAPI. } - else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) - || !string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase)) + else if (!string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase)) { param += " -level " + level; } @@ -1782,7 +1801,7 @@ namespace MediaBrowser.Controller.MediaEncoding return false; } - return request.EnableAutoStreamCopy; + return true; } public bool CanStreamCopyAudio(EncodingJobInfo state, MediaStream audioStream, IEnumerable<string> supportedAudioCodecs) @@ -1839,17 +1858,11 @@ namespace MediaBrowser.Controller.MediaEncoding } // Video bitrate must fall within requested value - if (request.AudioBitRate.HasValue) + if (request.AudioBitRate.HasValue + && audioStream.BitDepth.HasValue + && audioStream.BitRate.Value > request.AudioBitRate.Value) { - if (!audioStream.BitRate.HasValue || audioStream.BitRate.Value <= 0) - { - return false; - } - - if (audioStream.BitRate.Value > request.AudioBitRate.Value) - { - return false; - } + return false; } return request.EnableAutoStreamCopy; @@ -2151,9 +2164,10 @@ namespace MediaBrowser.Controller.MediaEncoding /// </summary> /// <param name="state">The state.</param> /// <param name="options">The options.</param> + /// <param name="segmentContainer">Segment Container.</param> /// <returns>System.String.</returns> /// <value>The fast seek command line parameter.</value> - public string GetFastSeekCommandLineParameter(EncodingJobInfo state, EncodingOptions options) + public string GetFastSeekCommandLineParameter(EncodingJobInfo state, EncodingOptions options, string segmentContainer) { var time = state.BaseRequest.StartTimeTicks ?? 0; var seekParam = string.Empty; @@ -2165,9 +2179,13 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.IsVideoRequest) { var outputVideoCodec = GetVideoEncoder(state, options); + var segmentFormat = GetSegmentFileExtension(segmentContainer).TrimStart('.'); // Important: If this is ever re-enabled, make sure not to use it with wtv because it breaks seeking + // Disable -noaccurate_seek on mpegts container due to the timestamps issue on some clients, + // but it's still required for fMP4 container otherwise the audio can't be synced to the video. if (!string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase) + && !string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase) && state.TranscodingType != TranscodingJobType.Progressive && !state.EnableBreakOnNonKeyFrames(outputVideoCodec) && (state.BaseRequest.StartTimeTicks ?? 0) > 0) @@ -2697,6 +2715,7 @@ namespace MediaBrowser.Controller.MediaEncoding var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty; var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); + var isV4l2Encoder = vidEncoder.Contains("h264_v4l2m2m", StringComparison.OrdinalIgnoreCase); var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); @@ -2725,6 +2744,10 @@ namespace MediaBrowser.Controller.MediaEncoding { outFormat = "nv12"; } + else if (isV4l2Encoder) + { + outFormat = "yuv420p"; + } // sw scale mainFilters.Add(swScaleFilter); @@ -2775,16 +2798,15 @@ namespace MediaBrowser.Controller.MediaEncoding var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !vidEncoder.Contains("nvenc", StringComparison.OrdinalIgnoreCase); - // legacy cuvid(resize/deint/sw) pipeline(copy-back) + // legacy cuvid pipeline(copy-back) if ((isSwDecoder && isSwEncoder) || !IsCudaFullSupported() - || !options.EnableEnhancedNvdecDecoder || !_mediaEncoder.SupportsFilter("alphasrc")) { return GetSwVidFilterChain(state, options, vidEncoder); } - // prefered nvdec + cuda filters + nvenc pipeline + // prefered nvdec/cuvid + cuda filters + nvenc pipeline return GetNvidiaVidFiltersPrefered(state, options, vidDecoder, vidEncoder); } @@ -2802,11 +2824,11 @@ namespace MediaBrowser.Controller.MediaEncoding var reqMaxH = state.BaseRequest.MaxHeight; var threeDFormat = state.MediaSource.Video3DFormat; - var isNvdecDecoder = vidDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase); + var isNvDecoder = vidDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase); var isNvencEncoder = vidEncoder.Contains("nvenc", StringComparison.OrdinalIgnoreCase); var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isNvencEncoder; - var isCuInCuOut = isNvdecDecoder && isNvencEncoder; + var isCuInCuOut = isNvDecoder && isNvencEncoder; var doubleRateDeint = options.DeinterlaceDoubleRate && (state.VideoStream?.AverageFrameRate ?? 60) <= 30; var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); @@ -2849,7 +2871,7 @@ namespace MediaBrowser.Controller.MediaEncoding } } - if (isNvdecDecoder) + if (isNvDecoder) { // INPUT cuda surface(vram) // hw deint @@ -2873,8 +2895,8 @@ namespace MediaBrowser.Controller.MediaEncoding } var memoryOutput = false; - var isUploadForOclTonemap = isSwDecoder && doCuTonemap; - if ((isNvdecDecoder && isSwEncoder) || isUploadForOclTonemap) + var isUploadForCuTonemap = isSwDecoder && doCuTonemap; + if ((isNvDecoder && isSwEncoder) || (isUploadForCuTonemap && hasSubs)) { memoryOutput = true; @@ -2884,7 +2906,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // OUTPUT yuv420p surface(memory) - if (isSwDecoder && isNvencEncoder) + if (isSwDecoder && isNvencEncoder && !isUploadForCuTonemap) { memoryOutput = true; } @@ -4270,11 +4292,6 @@ namespace MediaBrowser.Controller.MediaEncoding { return GetVideotoolboxVidDecoder(state, options, videoStream, bitDepth); } - - if (string.Equals(options.HardwareAccelerationType, "omx", StringComparison.OrdinalIgnoreCase)) - { - return GetOmxVidDecoder(state, options, videoStream, bitDepth); - } } var whichCodec = videoStream.Codec; @@ -4411,9 +4428,18 @@ namespace MediaBrowser.Controller.MediaEncoding // Nvidia cuda if (string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) { - if (options.EnableEnhancedNvdecDecoder && isCudaSupported && isCodecAvailable) + if (isCudaSupported && isCodecAvailable) { - return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty); + if (options.EnableEnhancedNvdecDecoder) + { + // set -threads 1 to nvdec decoder explicitly since it doesn't implement threading support. + return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty) + " -threads 1" + (isAv1 ? " -c:v av1" : string.Empty); + } + else + { + // cuvid decoder doesn't have threading issue. + return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty); + } } } @@ -4523,9 +4549,7 @@ namespace MediaBrowser.Controller.MediaEncoding return null; } - var hwSurface = IsCudaFullSupported() - && options.EnableEnhancedNvdecDecoder - && _mediaEncoder.SupportsFilter("alphasrc"); + var hwSurface = IsCudaFullSupported() && _mediaEncoder.SupportsFilter("alphasrc"); var is8bitSwFormatsNvdec = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8_10bitSwFormatsNvdec = is8bitSwFormatsNvdec || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); // TODO: add more 8/10/12bit and 4:4:4 formats for Nvdec after finishing the ffcheck tool @@ -4749,43 +4773,6 @@ namespace MediaBrowser.Controller.MediaEncoding return null; } - public string GetOmxVidDecoder(EncodingJobInfo state, EncodingOptions options, MediaStream videoStream, int bitDepth) - { - if (!OperatingSystem.IsLinux() - || !string.Equals(options.HardwareAccelerationType, "omx", StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - var is8bitSwFormatsOmx = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); - - if (is8bitSwFormatsOmx) - { - if (string.Equals("avc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) - || string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) - { - return GetHwDecoderName(options, "h264", "mmal", "h264", bitDepth); - } - - if (string.Equals("mpeg2video", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) - { - return GetHwDecoderName(options, "mpeg2", "mmal", "mpeg2video", bitDepth); - } - - if (string.Equals("mpeg4", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) - { - return GetHwDecoderName(options, "mpeg4", "mmal", "mpeg4", bitDepth); - } - - if (string.Equals("vc1", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) - { - return GetHwDecoderName(options, "vc1", "mmal", "vc1", bitDepth); - } - } - - return null; - } - /// <summary> /// Gets the number of threads. /// </summary> @@ -4850,7 +4837,7 @@ namespace MediaBrowser.Controller.MediaEncoding } } - public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions) + public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions, string segmentContainer) { var inputModifier = string.Empty; var probeSizeArgument = string.Empty; @@ -4886,7 +4873,7 @@ namespace MediaBrowser.Controller.MediaEncoding inputModifier = inputModifier.Trim(); - inputModifier += " " + GetFastSeekCommandLineParameter(state, encodingOptions); + inputModifier += " " + GetFastSeekCommandLineParameter(state, encodingOptions, segmentContainer); inputModifier = inputModifier.Trim(); if (state.InputProtocol == MediaProtocol.Rtsp) @@ -5193,13 +5180,13 @@ namespace MediaBrowser.Controller.MediaEncoding var threads = GetNumberOfThreads(state, encodingOptions, videoCodec); - var inputModifier = GetInputModifier(state, encodingOptions); + var inputModifier = GetInputModifier(state, encodingOptions, null); return string.Format( CultureInfo.InvariantCulture, "{0} {1}{2} {3} {4} -map_metadata -1 -map_chapters -1 -threads {5} {6}{7}{8} -y \"{9}\"", inputModifier, - GetInputArgument(state, encodingOptions), + GetInputArgument(state, encodingOptions, null), keyFrame, GetMapArgs(state), GetProgressiveVideoArguments(state, encodingOptions, videoCodec, defaultPreset), @@ -5381,13 +5368,13 @@ namespace MediaBrowser.Controller.MediaEncoding var threads = GetNumberOfThreads(state, encodingOptions, null); - var inputModifier = GetInputModifier(state, encodingOptions); + var inputModifier = GetInputModifier(state, encodingOptions, null); return string.Format( CultureInfo.InvariantCulture, "{0} {1}{7}{8} -threads {2}{3} {4} -id3v2_version 3 -write_id3v1 1{6} -y \"{5}\"", inputModifier, - GetInputArgument(state, encodingOptions), + GetInputArgument(state, encodingOptions, null), threads, " -vn", string.Join(' ', audioTranscodeParams), diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index c4affa567..4f6743590 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; using Jellyfin.Data.Entities; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; @@ -23,7 +24,7 @@ namespace MediaBrowser.Controller.MediaEncoding public int? OutputAudioBitrate; public int? OutputAudioChannels; - private TranscodeReason[] _transcodeReasons = null; + private TranscodeReason? _transcodeReasons = null; public EncodingJobInfo(TranscodingJobType jobType) { @@ -34,25 +35,23 @@ namespace MediaBrowser.Controller.MediaEncoding SupportedSubtitleCodecs = Array.Empty<string>(); } - public TranscodeReason[] TranscodeReasons + public TranscodeReason TranscodeReason { get { - if (_transcodeReasons == null) + if (!_transcodeReasons.HasValue) { if (BaseRequest.TranscodeReasons == null) { - return Array.Empty<TranscodeReason>(); + _transcodeReasons = 0; + return 0; } - _transcodeReasons = BaseRequest.TranscodeReasons - .Split(',') - .Where(i => !string.IsNullOrEmpty(i)) - .Select(v => (TranscodeReason)Enum.Parse(typeof(TranscodeReason), v, true)) - .ToArray(); + _ = Enum.TryParse<TranscodeReason>(BaseRequest.TranscodeReasons, out var reason); + _transcodeReasons = reason; } - return _transcodeReasons; + return _transcodeReasons.Value; } } diff --git a/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs index 4e7e26624..09840d2ee 100644 --- a/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs +++ b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs @@ -6,6 +6,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.MediaEncoding @@ -17,5 +18,17 @@ namespace MediaBrowser.Controller.MediaEncoding string mediaSourceId, int attachmentStreamIndex, CancellationToken cancellationToken); + + Task ExtractAllAttachments( + string inputFile, + MediaSourceInfo mediaSource, + string outputPath, + CancellationToken cancellationToken); + + Task ExtractAllAttachmentsExternal( + string inputArgument, + string id, + string outputPath, + CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index 89f3bdf46..828ecb2c5 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -233,7 +233,7 @@ namespace MediaBrowser.Controller.Playlists return base.IsVisible(user); } - if (user.Id == OwnerUserId) + if (user.Id.Equals(OwnerUserId)) { return true; } @@ -244,8 +244,8 @@ namespace MediaBrowser.Controller.Playlists return base.IsVisible(user); } - var userId = user.Id.ToString("N", CultureInfo.InvariantCulture); - return shares.Any(share => string.Equals(share.UserId, userId, StringComparison.OrdinalIgnoreCase)); + var userId = user.Id; + return shares.Any(share => Guid.TryParse(share.UserId, out var id) && id.Equals(userId)); } public override bool IsVisibleStandalone(User user) diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index 6134c0cf3..c2ca23386 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -39,6 +39,8 @@ namespace MediaBrowser.Controller.Session AdditionalUsers = Array.Empty<SessionUserInfo>(); PlayState = new PlayerStateInfo(); SessionControllers = Array.Empty<ISessionController>(); + NowPlayingQueue = Array.Empty<QueueItem>(); + NowPlayingQueueFullItems = Array.Empty<BaseItemDto>(); } public PlayerStateInfo PlayState { get; set; } @@ -219,7 +221,9 @@ namespace MediaBrowser.Controller.Session } } - public QueueItem[] NowPlayingQueue { get; set; } + public IReadOnlyList<QueueItem> NowPlayingQueue { get; set; } + + public IReadOnlyList<BaseItemDto> NowPlayingQueueFullItems { get; set; } public bool HasCustomDeviceName { get; set; } diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj index 41ac7038a..4a0624540 100644 --- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj +++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj @@ -27,7 +27,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index 3fd4cd731..142571e8f 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -83,6 +83,169 @@ namespace MediaBrowser.MediaEncoding.Attachments return (mediaAttachment, attachmentStream); } + public async Task ExtractAllAttachments( + string inputFile, + MediaSourceInfo mediaSource, + string outputPath, + CancellationToken cancellationToken) + { + var semaphore = _semaphoreLocks.GetOrAdd(outputPath, key => new SemaphoreSlim(1, 1)); + + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + if (!Directory.Exists(outputPath)) + { + await ExtractAllAttachmentsInternal( + _mediaEncoder.GetInputArgument(inputFile, mediaSource), + outputPath, + false, + cancellationToken).ConfigureAwait(false); + } + } + finally + { + semaphore.Release(); + } + } + + public async Task ExtractAllAttachmentsExternal( + string inputArgument, + string id, + string outputPath, + CancellationToken cancellationToken) + { + var semaphore = _semaphoreLocks.GetOrAdd(outputPath, key => new SemaphoreSlim(1, 1)); + + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + if (!File.Exists(Path.Join(outputPath, id))) + { + await ExtractAllAttachmentsInternal( + inputArgument, + outputPath, + true, + cancellationToken).ConfigureAwait(false); + + if (Directory.Exists(outputPath)) + { + File.Create(Path.Join(outputPath, id)); + } + } + } + finally + { + semaphore.Release(); + } + } + + private async Task ExtractAllAttachmentsInternal( + string inputPath, + string outputPath, + bool isExternal, + CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(inputPath)) + { + throw new ArgumentNullException(nameof(inputPath)); + } + + if (string.IsNullOrEmpty(outputPath)) + { + throw new ArgumentNullException(nameof(outputPath)); + } + + Directory.CreateDirectory(outputPath); + + var processArgs = string.Format( + CultureInfo.InvariantCulture, + "-dump_attachment:t \"\" -y -i {0} -t 0 -f null null", + inputPath); + + int exitCode; + + using (var process = new Process + { + StartInfo = new ProcessStartInfo + { + Arguments = processArgs, + FileName = _mediaEncoder.EncoderPath, + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + WorkingDirectory = outputPath, + ErrorDialog = false + }, + EnableRaisingEvents = true + }) + { + _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); + + process.Start(); + + var ranToCompletion = await ProcessExtensions.WaitForExitAsync(process, cancellationToken).ConfigureAwait(false); + + if (!ranToCompletion) + { + try + { + _logger.LogWarning("Killing ffmpeg attachment extraction process"); + process.Kill(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error killing attachment extraction process"); + } + } + + exitCode = ranToCompletion ? process.ExitCode : -1; + } + + var failed = false; + + if (exitCode != 0) + { + if (isExternal && exitCode == 1) + { + // ffmpeg returns exitCode 1 because there is no video or audio stream + // this can be ignored + } + else + { + failed = true; + + _logger.LogWarning("Deleting extracted attachments {Path} due to failure: {ExitCode}", outputPath, exitCode); + try + { + Directory.Delete(outputPath); + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting extracted attachments {Path}", outputPath); + } + } + } + else if (!Directory.Exists(outputPath)) + { + failed = true; + } + + if (failed) + { + _logger.LogError("ffmpeg attachment extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath); + + throw new InvalidOperationException( + string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputPath)); + } + else + { + _logger.LogInformation("ffmpeg attachment extraction completed for {Path} to {Path}", inputPath, outputPath); + } + } + private async Task<Stream> GetAttachmentStream( MediaSourceInfo mediaSource, MediaAttachment mediaAttachment, diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index fe3069934..20d372d7a 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -44,18 +44,7 @@ namespace MediaBrowser.MediaEncoding.Encoder "mpeg4_cuvid", "vp8_cuvid", "vp9_cuvid", - "av1_cuvid", - "h264_mmal", - "mpeg2_mmal", - "mpeg4_mmal", - "vc1_mmal", - "h264_opencl", - "hevc_opencl", - "mpeg2_opencl", - "mpeg4_opencl", - "vp8_opencl", - "vp9_opencl", - "vc1_opencl" + "av1_cuvid" }; private static readonly string[] _requiredEncoders = new[] @@ -82,8 +71,6 @@ namespace MediaBrowser.MediaEncoding.Encoder "hevc_nvenc", "h264_vaapi", "hevc_vaapi", - "h264_omx", - "hevc_omx", "h264_v4l2m2m", "h264_videotoolbox", "hevc_videotoolbox" diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index 47de4edff..7c70d37a6 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -27,7 +27,7 @@ <ItemGroup> <PackageReference Include="BDInfo" Version="0.7.6.2" /> - <PackageReference Include="libse" Version="3.6.4" /> + <PackageReference Include="libse" Version="3.6.5" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" /> <PackageReference Include="UTF.Unknown" Version="2.5.0" /> @@ -40,7 +40,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs b/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs index a1cef7a9f..c9e948780 100644 --- a/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs +++ b/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs @@ -12,7 +12,7 @@ namespace MediaBrowser.MediaEncoding.Probing public class MediaChapter { [JsonPropertyName("id")] - public int Id { get; set; } + public long Id { get; set; } [JsonPropertyName("time_base")] public string TimeBase { get; set; } diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 4c8f19604..3f78d0d42 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -613,6 +613,17 @@ namespace MediaBrowser.MediaEncoding.Probing } /// <summary> + /// Determines whether a stream code time base is double the frame rate. + /// </summary> + /// <param name="averageFrameRate">average frame rate.</param> + /// <param name="codecTimeBase">codec time base string.</param> + /// <returns>true if the codec time base is double the frame rate.</returns> + internal static bool IsCodecTimeBaseDoubleTheFrameRate(float? averageFrameRate, string codecTimeBase) + { + return MathF.Abs(((averageFrameRate ?? 0) * (GetFrameRate(codecTimeBase) ?? 0)) - 0.5f) <= float.Epsilon; + } + + /// <summary> /// Converts ffprobe stream info to our MediaStream class. /// </summary> /// <param name="isAudio">if set to <c>true</c> [is info].</param> @@ -691,9 +702,9 @@ namespace MediaBrowser.MediaEncoding.Probing if (string.IsNullOrEmpty(stream.Title)) { - // mp4 missing track title workaround: fall back to handler_name if populated + // mp4 missing track title workaround: fall back to handler_name if populated and not the default "SoundHandler" string handlerName = GetDictionaryValue(streamInfo.Tags, "handler_name"); - if (!string.IsNullOrEmpty(handlerName)) + if (!string.IsNullOrEmpty(handlerName) && !string.Equals(handlerName, "SoundHandler", StringComparison.OrdinalIgnoreCase)) { stream.Title = handlerName; } @@ -706,6 +717,7 @@ namespace MediaBrowser.MediaEncoding.Probing stream.LocalizedUndefined = _localization.GetLocalizedString("Undefined"); stream.LocalizedDefault = _localization.GetLocalizedString("Default"); stream.LocalizedForced = _localization.GetLocalizedString("Forced"); + stream.LocalizedExternal = _localization.GetLocalizedString("External"); if (string.IsNullOrEmpty(stream.Title)) { @@ -722,17 +734,16 @@ namespace MediaBrowser.MediaEncoding.Probing stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate); stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate); + bool videoInterlaced = !string.IsNullOrWhiteSpace(streamInfo.FieldOrder) + && !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase); + // Some interlaced H.264 files in mp4 containers using MBAFF coding aren't flagged as being interlaced by FFprobe, // so for H.264 files we also calculate the frame rate from the codec time base and check if it is double the reported - // frame rate (both rounded to the nearest integer) to determine if the file is interlaced - int roundedTimeBaseFPS = Convert.ToInt32(1 / GetFrameRate(stream.CodecTimeBase) ?? 0); - int roundedDoubleFrameRate = Convert.ToInt32(stream.AverageFrameRate * 2 ?? 0); + // frame rate to determine if the file is interlaced - bool videoInterlaced = !string.IsNullOrWhiteSpace(streamInfo.FieldOrder) - && !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase); bool h264MbaffCoded = string.Equals(stream.Codec, "h264", StringComparison.OrdinalIgnoreCase) && string.IsNullOrWhiteSpace(streamInfo.FieldOrder) - && roundedTimeBaseFPS == roundedDoubleFrameRate; + && IsCodecTimeBaseDoubleTheFrameRate(stream.AverageFrameRate, stream.CodecTimeBase); if (videoInterlaced || h264MbaffCoded) { diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 51917b50e..06931ac3b 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -32,7 +32,7 @@ namespace MediaBrowser.Model.Configuration DeinterlaceMethod = "yadif"; EnableDecodingColorDepth10Hevc = true; EnableDecodingColorDepth10Vp9 = true; - EnableEnhancedNvdecDecoder = true; + EnableEnhancedNvdecDecoder = false; PreferSystemNativeHwDecoder = true; EnableIntelLowPowerH264HwEncoder = false; EnableIntelLowPowerHevcHwEncoder = false; diff --git a/MediaBrowser.Model/Dlna/AudioOptions.cs b/MediaBrowser.Model/Dlna/AudioOptions.cs index 4d4d8d78c..33755e746 100644 --- a/MediaBrowser.Model/Dlna/AudioOptions.cs +++ b/MediaBrowser.Model/Dlna/AudioOptions.cs @@ -27,6 +27,8 @@ namespace MediaBrowser.Model.Dlna public bool ForceDirectStream { get; set; } + public bool AllowAudioStreamCopy { get; set; } + public Guid ItemId { get; set; } public MediaSourceInfo[] MediaSources { get; set; } diff --git a/MediaBrowser.Model/Dlna/DlnaProfileType.cs b/MediaBrowser.Model/Dlna/DlnaProfileType.cs index e30ed0f3c..c1a663bf1 100644 --- a/MediaBrowser.Model/Dlna/DlnaProfileType.cs +++ b/MediaBrowser.Model/Dlna/DlnaProfileType.cs @@ -6,6 +6,7 @@ namespace MediaBrowser.Model.Dlna { Audio = 0, Video = 1, - Photo = 2 + Photo = 2, + Subtitle = 3 } } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index d2ca21150..93b0a454c 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -15,6 +15,12 @@ namespace MediaBrowser.Model.Dlna { public class StreamBuilder { + // Aliases + internal const TranscodeReason ContainerReasons = TranscodeReason.ContainerNotSupported | TranscodeReason.ContainerBitrateExceedsLimit; + internal const TranscodeReason AudioReasons = TranscodeReason.AudioCodecNotSupported | TranscodeReason.AudioBitrateNotSupported | TranscodeReason.AudioChannelsNotSupported | TranscodeReason.AudioProfileNotSupported | TranscodeReason.AudioSampleRateNotSupported | TranscodeReason.SecondaryAudioNotSupported | TranscodeReason.AudioBitDepthNotSupported | TranscodeReason.AudioIsExternal; + internal const TranscodeReason VideoReasons = TranscodeReason.VideoCodecNotSupported | TranscodeReason.VideoResolutionNotSupported | TranscodeReason.AnamorphicVideoNotSupported | TranscodeReason.InterlacedVideoNotSupported | TranscodeReason.VideoBitDepthNotSupported | TranscodeReason.VideoBitrateNotSupported | TranscodeReason.VideoFramerateNotSupported | TranscodeReason.VideoLevelNotSupported | TranscodeReason.RefFramesNotSupported; + internal const TranscodeReason DirectStreamReasons = AudioReasons | TranscodeReason.ContainerNotSupported; + private readonly ILogger _logger; private readonly ITranscoderSupport _transcoderSupport; @@ -143,7 +149,7 @@ namespace MediaBrowser.Model.Dlna }).ThenBy(streams.IndexOf); } - private static TranscodeReason? GetTranscodeReasonForFailedCondition(ProfileCondition condition) + private static TranscodeReason GetTranscodeReasonForFailedCondition(ProfileCondition condition) { switch (condition.Property) { @@ -161,7 +167,7 @@ namespace MediaBrowser.Model.Dlna case ProfileConditionValue.Has64BitOffsets: // TODO - return null; + return 0; case ProfileConditionValue.Height: return TranscodeReason.VideoResolutionNotSupported; @@ -171,7 +177,7 @@ namespace MediaBrowser.Model.Dlna case ProfileConditionValue.IsAvc: // TODO - return null; + return 0; case ProfileConditionValue.IsInterlaced: return TranscodeReason.InterlacedVideoNotSupported; @@ -181,15 +187,15 @@ namespace MediaBrowser.Model.Dlna case ProfileConditionValue.NumAudioStreams: // TODO - return null; + return 0; case ProfileConditionValue.NumVideoStreams: // TODO - return null; + return 0; case ProfileConditionValue.PacketLength: // TODO - return null; + return 0; case ProfileConditionValue.RefFrames: return TranscodeReason.RefFramesNotSupported; @@ -217,17 +223,17 @@ namespace MediaBrowser.Model.Dlna case ProfileConditionValue.VideoTimestamp: // TODO - return null; + return 0; case ProfileConditionValue.Width: return TranscodeReason.VideoResolutionNotSupported; default: - return null; + return 0; } } - public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile profile, DlnaProfileType type) + public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile profile, DlnaProfileType type, DirectPlayProfile playProfile = null) { if (string.IsNullOrEmpty(inputContainer)) { @@ -236,16 +242,12 @@ namespace MediaBrowser.Model.Dlna var formats = ContainerProfile.SplitValue(inputContainer); - if (formats.Length == 1) - { - return formats[0]; - } - if (profile != null) { + var playProfiles = playProfile == null ? profile.DirectPlayProfiles : new[] { playProfile }; foreach (var format in formats) { - foreach (var directPlayProfile in profile.DirectPlayProfiles) + foreach (var directPlayProfile in playProfiles) { if (directPlayProfile.Type == type && directPlayProfile.SupportsContainer(format)) @@ -287,69 +289,27 @@ namespace MediaBrowser.Model.Dlna var audioStream = item.GetDefaultAudioStream(null); - var directPlayInfo = GetAudioDirectPlayMethods(item, audioStream, options); + var directPlayInfo = GetAudioDirectPlayProfile(item, audioStream, options); - var directPlayMethods = directPlayInfo.PlayMethods; - var transcodeReasons = directPlayInfo.TranscodeReasons.ToList(); + var directPlayMethod = directPlayInfo.PlayMethod; + var transcodeReasons = directPlayInfo.TranscodeReasons; int? inputAudioChannels = audioStream?.Channels; int? inputAudioBitrate = audioStream?.BitDepth; int? inputAudioSampleRate = audioStream?.SampleRate; int? inputAudioBitDepth = audioStream?.BitDepth; - if (directPlayMethods.Any()) + if (directPlayMethod.HasValue) { - string audioCodec = audioStream?.Codec; + var profile = options.Profile; + var audioFailureConditions = GetProfileConditionsForAudio(profile.CodecProfiles, item.Container, audioStream?.Codec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, true); + var audioFailureReasons = AggregateFailureConditions(item, profile, "AudioCodecProfile", audioFailureConditions); + transcodeReasons |= audioFailureReasons; - // Make sure audio codec profiles are satisfied - var conditions = new List<ProfileCondition>(); - foreach (var i in options.Profile.CodecProfiles) + if (audioFailureReasons == 0) { - if (i.Type == CodecType.Audio && i.ContainsAnyCodec(audioCodec, item.Container)) - { - bool applyConditions = true; - foreach (ProfileCondition applyCondition in i.ApplyConditions) - { - if (!ConditionProcessor.IsAudioConditionSatisfied(applyCondition, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth)) - { - LogConditionFailure(options.Profile, "AudioCodecProfile", applyCondition, item); - applyConditions = false; - break; - } - } - - if (applyConditions) - { - conditions.AddRange(i.Conditions); - } - } - } - - bool all = true; - foreach (ProfileCondition c in conditions) - { - if (!ConditionProcessor.IsAudioConditionSatisfied(c, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth)) - { - LogConditionFailure(options.Profile, "AudioCodecProfile", c, item); - var transcodeReason = GetTranscodeReasonForFailedCondition(c); - if (transcodeReason.HasValue) - { - transcodeReasons.Add(transcodeReason.Value); - } - - all = false; - break; - } - } - - if (all) - { - if (directPlayMethods.Contains(PlayMethod.DirectStream)) - { - playlistItem.PlayMethod = PlayMethod.DirectStream; - } - - playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio); + playlistItem.PlayMethod = directPlayMethod.Value; + playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio, directPlayInfo.Profile); return playlistItem; } @@ -374,45 +334,9 @@ namespace MediaBrowser.Model.Dlna return null; } - SetStreamInfoOptionsFromTranscodingProfile(playlistItem, transcodingProfile); - - var audioCodecProfiles = new List<CodecProfile>(); - foreach (var i in options.Profile.CodecProfiles) - { - if (i.Type == CodecType.Audio && i.ContainsAnyCodec(transcodingProfile.AudioCodec, transcodingProfile.Container)) - { - audioCodecProfiles.Add(i); - } - - if (audioCodecProfiles.Count >= 1) - { - break; - } - } - - var audioTranscodingConditions = new List<ProfileCondition>(); - foreach (var i in audioCodecProfiles) - { - bool applyConditions = true; - foreach (var applyCondition in i.ApplyConditions) - { - if (!ConditionProcessor.IsAudioConditionSatisfied(applyCondition, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth)) - { - LogConditionFailure(options.Profile, "AudioCodecProfile", applyCondition, item); - applyConditions = false; - break; - } - } - - if (applyConditions) - { - foreach (ProfileCondition c in i.Conditions) - { - audioTranscodingConditions.Add(c); - } - } - } + SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile); + var audioTranscodingConditions = GetProfileConditionsForAudio(options.Profile.CodecProfiles, transcodingProfile.Container, transcodingProfile.AudioCodec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, false).ToArray(); ApplyTranscodingConditions(playlistItem, audioTranscodingConditions, null, true, true); // Honor requested max channels @@ -434,23 +358,13 @@ namespace MediaBrowser.Model.Dlna playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate); } - playlistItem.TranscodeReasons = transcodeReasons.ToArray(); + playlistItem.TranscodeReasons = transcodeReasons; return playlistItem; } - private static long? GetBitrateForDirectPlayCheck(MediaSourceInfo item, AudioOptions options, bool isAudio) - { - if (item.Protocol == MediaProtocol.File) - { - return options.Profile.MaxStaticBitrate; - } - - return options.GetMaxBitrate(isAudio); - } - - private (IEnumerable<PlayMethod> PlayMethods, IEnumerable<TranscodeReason> TranscodeReasons) GetAudioDirectPlayMethods(MediaSourceInfo item, MediaStream audioStream, AudioOptions options) + private (DirectPlayProfile Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, AudioOptions options) { - DirectPlayProfile directPlayProfile = options.Profile.DirectPlayProfiles + var directPlayProfile = options.Profile.DirectPlayProfiles .FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(x, item, audioStream)); if (directPlayProfile == null) @@ -461,64 +375,56 @@ namespace MediaBrowser.Model.Dlna item.Path ?? "Unknown path", audioStream.Codec ?? "Unknown codec"); - return (Enumerable.Empty<PlayMethod>(), GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles)); + return (null, null, GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles)); } var playMethods = new List<PlayMethod>(); - var transcodeReasons = new List<TranscodeReason>(); + TranscodeReason transcodeReasons = 0; - // While options takes the network and other factors into account. Only applies to direct stream - if (item.SupportsDirectStream) + // The profile describes what the device supports + // If device requirements are satisfied then allow both direct stream and direct play + if (item.SupportsDirectPlay) { - if (IsAudioEligibleForDirectPlay(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream)) + if (IsItemBitrateEligibleForDirectPlay(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectPlay)) { - if (options.EnableDirectStream) + if (options.EnableDirectPlay) { - playMethods.Add(PlayMethod.DirectStream); + return (directPlayProfile, PlayMethod.DirectPlay, 0); } } else { - transcodeReasons.Add(TranscodeReason.ContainerBitrateExceedsLimit); + transcodeReasons |= TranscodeReason.ContainerBitrateExceedsLimit; } } - // The profile describes what the device supports - // If device requirements are satisfied then allow both direct stream and direct play - if (item.SupportsDirectPlay) + // While options takes the network and other factors into account. Only applies to direct stream + if (item.SupportsDirectStream) { - if (IsAudioEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, PlayMethod.DirectPlay)) + if (IsItemBitrateEligibleForDirectPlay(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream)) { - if (options.EnableDirectPlay) + if (options.EnableDirectStream) { - playMethods.Add(PlayMethod.DirectPlay); + return (directPlayProfile, PlayMethod.DirectStream, transcodeReasons); } } else { - transcodeReasons.Add(TranscodeReason.ContainerBitrateExceedsLimit); + transcodeReasons |= TranscodeReason.ContainerBitrateExceedsLimit; } } - if (playMethods.Count > 0) - { - transcodeReasons.Clear(); - } - else - { - transcodeReasons = transcodeReasons.Distinct().ToList(); - } - - return (playMethods, transcodeReasons); + return (directPlayProfile, null, transcodeReasons); } - private static List<TranscodeReason> GetTranscodeReasonsFromDirectPlayProfile(MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<DirectPlayProfile> directPlayProfiles) + private static TranscodeReason GetTranscodeReasonsFromDirectPlayProfile(MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<DirectPlayProfile> directPlayProfiles) { var mediaType = videoStream == null ? DlnaProfileType.Audio : DlnaProfileType.Video; var containerSupported = false; var audioSupported = false; var videoSupported = false; + TranscodeReason reasons = 0; foreach (var profile in directPlayProfiles) { @@ -541,20 +447,20 @@ namespace MediaBrowser.Model.Dlna var list = new List<TranscodeReason>(); if (!containerSupported) { - list.Add(TranscodeReason.ContainerNotSupported); + reasons |= TranscodeReason.ContainerNotSupported; } if (videoStream != null && !videoSupported) { - list.Add(TranscodeReason.VideoCodecNotSupported); + reasons |= TranscodeReason.VideoCodecNotSupported; } if (audioStream != null && !audioSupported) { - list.Add(TranscodeReason.AudioCodecNotSupported); + reasons |= TranscodeReason.AudioCodecNotSupported; } - return list; + return reasons; } private static int? GetDefaultSubtitleStreamIndex(MediaSourceInfo item, SubtitleProfile[] subtitleProfiles) @@ -599,30 +505,29 @@ namespace MediaBrowser.Model.Dlna return item.DefaultSubtitleStreamIndex; } - private static void SetStreamInfoOptionsFromTranscodingProfile(StreamInfo playlistItem, TranscodingProfile transcodingProfile) + private static void SetStreamInfoOptionsFromTranscodingProfile(MediaSourceInfo item, StreamInfo playlistItem, TranscodingProfile transcodingProfile) { - if (string.IsNullOrEmpty(transcodingProfile.AudioCodec)) - { - playlistItem.AudioCodecs = Array.Empty<string>(); - } - else - { - playlistItem.AudioCodecs = transcodingProfile.AudioCodec.Split(','); - } + var container = transcodingProfile.Container; + var protocol = transcodingProfile.Protocol; - playlistItem.Container = transcodingProfile.Container; - playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength; - playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo; + item.TranscodingContainer = container; + item.TranscodingSubProtocol = protocol; - if (string.IsNullOrEmpty(transcodingProfile.VideoCodec)) + if (playlistItem.PlayMethod == PlayMethod.Transcode) { - playlistItem.VideoCodecs = Array.Empty<string>(); + playlistItem.Container = container; + playlistItem.SubProtocol = protocol; } - else + + playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo; + if (!string.IsNullOrEmpty(transcodingProfile.MaxAudioChannels) + && int.TryParse(transcodingProfile.MaxAudioChannels, NumberStyles.Any, CultureInfo.InvariantCulture, out int transcodingMaxAudioChannels)) { - playlistItem.VideoCodecs = transcodingProfile.VideoCodec.Split(','); + playlistItem.TranscodingMaxAudioChannels = transcodingMaxAudioChannels; } + playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength; + playlistItem.CopyTimestamps = transcodingProfile.CopyTimestamps; playlistItem.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest; playlistItem.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode; @@ -638,14 +543,21 @@ namespace MediaBrowser.Model.Dlna { playlistItem.SegmentLength = transcodingProfile.SegmentLength; } + } - playlistItem.SubProtocol = transcodingProfile.Protocol; + private static void SetStreamInfoOptionsFromDirectPlayProfile(VideoOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile directPlayProfile) + { + var container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile); + var protocol = "http"; - if (!string.IsNullOrEmpty(transcodingProfile.MaxAudioChannels) - && int.TryParse(transcodingProfile.MaxAudioChannels, NumberStyles.Any, CultureInfo.InvariantCulture, out int transcodingMaxAudioChannels)) - { - playlistItem.TranscodingMaxAudioChannels = transcodingMaxAudioChannels; - } + item.TranscodingContainer = container; + item.TranscodingSubProtocol = protocol; + + playlistItem.Container = container; + playlistItem.SubProtocol = protocol; + + playlistItem.VideoCodecs = new[] { item.VideoStream.Codec }; + playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec); } private StreamInfo BuildVideoItem(MediaSourceInfo item, VideoOptions options) @@ -674,13 +586,29 @@ namespace MediaBrowser.Model.Dlna playlistItem.AudioStreamIndex = audioStream.Index; } + // Collect candidate audio streams + IEnumerable<MediaStream> candidateAudioStreams = audioStream == null ? Array.Empty<MediaStream>() : new[] { audioStream }; + if (!options.AudioStreamIndex.HasValue || options.AudioStreamIndex < 0) + { + if (audioStream?.IsDefault == true) + { + candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.IsDefault); + } + else + { + candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.Language == audioStream?.Language); + } + } + + candidateAudioStreams = candidateAudioStreams.ToArray(); + var videoStream = item.VideoStream; - // TODO: This doesn't account for situations where the device is able to handle the media's bitrate, but the connection isn't fast enough - var directPlayEligibilityResult = IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, subtitleStream, audioStream, options, PlayMethod.DirectPlay); - var directStreamEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, subtitleStream, audioStream, options, PlayMethod.DirectStream); - bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult.DirectPlay); - bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamEligibilityResult.DirectPlay); + var directPlayEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectPlay); + var directStreamEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectStream); + bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult == 0); + bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directPlayEligibilityResult == 0); + var transcodeReasons = directPlayEligibilityResult | directStreamEligibilityResult; _logger.LogDebug( "Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}", @@ -689,189 +617,305 @@ namespace MediaBrowser.Model.Dlna isEligibleForDirectPlay, isEligibleForDirectStream); - var transcodeReasons = new List<TranscodeReason>(); - + DirectPlayProfile directPlayProfile = null; if (isEligibleForDirectPlay || isEligibleForDirectStream) { // See if it can be direct played - var directPlayInfo = GetVideoDirectPlayProfile(options, item, videoStream, audioStream, isEligibleForDirectStream); + var directPlayInfo = GetVideoDirectPlayProfile(options, item, videoStream, audioStream, candidateAudioStreams, subtitleStream, isEligibleForDirectPlay, isEligibleForDirectStream); var directPlay = directPlayInfo.PlayMethod; + transcodeReasons |= directPlayInfo.TranscodeReasons; if (directPlay != null) { + directPlayProfile = directPlayInfo.Profile; playlistItem.PlayMethod = directPlay.Value; - playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video); + playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile); + playlistItem.VideoCodecs = new[] { videoStream.Codec }; + + if (directPlay == PlayMethod.DirectPlay) + { + playlistItem.SubProtocol = "http"; + + var audioStreamIndex = directPlayInfo.AudioStreamIndex ?? audioStream?.Index; + if (audioStreamIndex.HasValue) + { + playlistItem.AudioStreamIndex = audioStreamIndex; + playlistItem.AudioCodecs = new[] { item.GetMediaStream(MediaStreamType.Audio, audioStreamIndex.Value)?.Codec }; + } + } + else if (directPlay == PlayMethod.DirectStream) + { + playlistItem.AudioStreamIndex = audioStream?.Index; + if (audioStream != null) + { + playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec); + } + + SetStreamInfoOptionsFromDirectPlayProfile(options, item, playlistItem, directPlayProfile); + BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStreams, directPlayProfile.Container, directPlayProfile.VideoCodec, directPlayProfile.AudioCodec); + } if (subtitleStream != null) { - var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, directPlay.Value, _transcoderSupport, item.Container, null); + var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, directPlay.Value, _transcoderSupport, directPlayProfile.Container, null); playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method; playlistItem.SubtitleFormat = subtitleProfile.Format; } - - return playlistItem; } - transcodeReasons.AddRange(directPlayInfo.TranscodeReasons); + _logger.LogInformation( + "DirectPlay Result for Profile: {0}, Path: {1}, PlayMethod: {2}, AudioStreamIndex: {3}, SubtitleStreamIndex: {4}, Reasons: {5}", + options.Profile.Name ?? "Anonymous Profile", + item.Path ?? "Unknown path", + directPlayInfo.PlayMethod, + directPlayInfo.AudioStreamIndex ?? audioStream?.Index, + playlistItem.SubtitleStreamIndex, + directPlayInfo.TranscodeReasons); } - if (directPlayEligibilityResult.Reason.HasValue) + playlistItem.TranscodeReasons = transcodeReasons; + + if (playlistItem.PlayMethod != PlayMethod.DirectStream || !options.EnableDirectStream) { - transcodeReasons.Add(directPlayEligibilityResult.Reason.Value); + // Can't direct play, find the transcoding profile + // If we do this for direct-stream we will overwrite the info + var transcodingProfile = GetVideoTranscodeProfile(item, options, videoStream, audioStream, candidateAudioStreams, subtitleStream, playlistItem); + if (transcodingProfile != null) + { + SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile); + + BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStreams, transcodingProfile.Container, transcodingProfile.VideoCodec, transcodingProfile.AudioCodec); + + if (subtitleStream != null) + { + var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.Transcode, _transcoderSupport, transcodingProfile.Container, transcodingProfile.Protocol); + + playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method; + playlistItem.SubtitleFormat = subtitleProfile.Format; + playlistItem.SubtitleCodecs = new[] { subtitleProfile.Format }; + } + + if (playlistItem.PlayMethod != PlayMethod.DirectPlay) + { + playlistItem.PlayMethod = PlayMethod.Transcode; + + if ((playlistItem.TranscodeReasons & (VideoReasons | TranscodeReason.ContainerBitrateExceedsLimit)) != 0) + { + ApplyTranscodingConditions(playlistItem, transcodingProfile.Conditions, null, true, true); + } + } + } } - if (directStreamEligibilityResult.Reason.HasValue) + _logger.LogInformation( + "StreamBuilder.BuildVideoItem( Profile={0}, Path={1}, AudioStreamIndex={2}, SubtitleStreamIndex={3} ) => ( PlayMethod={4}, TranscodeReason={5} ) {6}", + options.Profile.Name ?? "Anonymous Profile", + item.Path ?? "Unknown path", + options.AudioStreamIndex, + options.SubtitleStreamIndex, + playlistItem.PlayMethod, + playlistItem.TranscodeReasons, + playlistItem.ToUrl("media:", "<token>")); + + item.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile); + return playlistItem; + } + + private TranscodingProfile GetVideoTranscodeProfile(MediaSourceInfo item, VideoOptions options, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, MediaStream subtitleStream, StreamInfo playlistItem) + { + if (!(item.SupportsTranscoding || item.SupportsDirectStream)) { - transcodeReasons.Add(directStreamEligibilityResult.Reason.Value); + return null; } - // Can't direct play, find the transcoding profile - TranscodingProfile transcodingProfile = null; - foreach (var i in options.Profile.TranscodingProfiles) + var transcodingProfiles = options.Profile.TranscodingProfiles + .Where(i => i.Type == playlistItem.MediaType && i.Context == options.Context); + + if (options.AllowVideoStreamCopy) { - if (i.Type == playlistItem.MediaType && i.Context == options.Context) + // prefer direct copy profile + float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; + TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp; + int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio); + int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video); + + transcodingProfiles = transcodingProfiles.ToLookup(transcodingProfile => { - transcodingProfile = i; - break; - } + var videoCodecs = ContainerProfile.SplitValue(transcodingProfile.VideoCodec); + + if (ContainerProfile.ContainsContainer(videoCodecs, item.VideoStream.Codec)) + { + var videoCodec = transcodingProfile.VideoCodec; + var container = transcodingProfile.Container; + var appliedVideoConditions = options.Profile.CodecProfiles + .Where(i => i.Type == CodecType.Video && + i.ContainsAnyCodec(videoCodec, container)) + .Select(i => + i.ApplyConditions.Any(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC))); + var conditionsSatisfied = !appliedVideoConditions.Any() || !appliedVideoConditions.Any(satisfied => !satisfied); + return conditionsSatisfied ? 1 : 2; + } + + return 3; + }) + .OrderBy(lookup => lookup.Key) + .SelectMany(lookup => lookup); } - if (transcodingProfile != null) + return transcodingProfiles.FirstOrDefault(); + } + + private void BuildStreamVideoItem(StreamInfo playlistItem, VideoOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, string container, string videoCodec, string audioCodec) + { + // prefer matching video codecs + var videoCodecs = ContainerProfile.SplitValue(videoCodec); + var directVideoCodec = ContainerProfile.ContainsContainer(videoCodecs, videoStream.Codec) ? videoStream.Codec : null; + playlistItem.VideoCodecs = directVideoCodec != null ? new[] { directVideoCodec } : videoCodecs; + + // copy video codec options as a starting point, this applies to transcode and direct-stream + playlistItem.MaxFramerate = videoStream.AverageFrameRate; + var qualifier = videoStream.Codec; + if (videoStream.Level.HasValue) { - if (!item.SupportsTranscoding) + playlistItem.SetOption(qualifier, "level", videoStream.Level.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (videoStream.BitDepth.HasValue) + { + playlistItem.SetOption(qualifier, "videobitdepth", videoStream.BitDepth.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (!string.IsNullOrEmpty(videoStream.Profile)) + { + playlistItem.SetOption(qualifier, "profile", videoStream.Profile.ToLowerInvariant()); + } + + if (videoStream.Level != 0) + { + playlistItem.SetOption(qualifier, "level", videoStream.Level.ToString()); + } + + // prefer matching audio codecs, could do beter here + var audioCodecs = ContainerProfile.SplitValue(audioCodec); + var directAudioStream = candidateAudioStreams.FirstOrDefault(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec)); + playlistItem.AudioCodecs = audioCodecs; + if (directAudioStream != null) + { + audioStream = directAudioStream; + playlistItem.AudioStreamIndex = audioStream.Index; + playlistItem.AudioCodecs = new[] { audioStream.Codec }; + + // copy matching audio codec options + playlistItem.AudioSampleRate = audioStream.SampleRate; + playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels.ToString()); + + if (!string.IsNullOrEmpty(audioStream.Profile)) { - return null; + playlistItem.SetOption(audioStream.Codec, "profile", audioStream.Profile.ToLowerInvariant()); } - if (subtitleStream != null) + if (audioStream.Level != 0) { - var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.Transcode, _transcoderSupport, transcodingProfile.Container, transcodingProfile.Protocol); - - playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method; - playlistItem.SubtitleFormat = subtitleProfile.Format; - playlistItem.SubtitleCodecs = new[] { subtitleProfile.Format }; + playlistItem.SetOption(audioStream.Codec, "level", audioStream.Level.ToString()); } + } - playlistItem.PlayMethod = PlayMethod.Transcode; + int? width = videoStream?.Width; + int? height = videoStream?.Height; + int? bitDepth = videoStream?.BitDepth; + int? videoBitrate = videoStream?.BitRate; + double? videoLevel = videoStream?.Level; + string videoProfile = videoStream?.Profile; + float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; + bool? isAnamorphic = videoStream?.IsAnamorphic; + bool? isInterlaced = videoStream?.IsInterlaced; + string videoCodecTag = videoStream?.CodecTag; + bool? isAvc = videoStream?.IsAVC; - SetStreamInfoOptionsFromTranscodingProfile(playlistItem, transcodingProfile); + TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp; + int? packetLength = videoStream?.PacketLength; + int? refFrames = videoStream?.RefFrames; + + int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio); + int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video); - var isFirstAppliedCodecProfile = true; - foreach (var i in options.Profile.CodecProfiles) + var appliedVideoConditions = options.Profile.CodecProfiles + .Where(i => i.Type == CodecType.Video && + i.ContainsAnyCodec(videoCodec, container) && + i.ApplyConditions.Any(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))); + var isFirstAppliedCodecProfile = true; + foreach (var i in appliedVideoConditions) + { + var transcodingVideoCodecs = ContainerProfile.SplitValue(videoCodec); + foreach (var transcodingVideoCodec in transcodingVideoCodecs) { - if (i.Type == CodecType.Video && i.ContainsAnyCodec(transcodingProfile.VideoCodec, transcodingProfile.Container)) + if (i.ContainsAnyCodec(transcodingVideoCodec, container)) { - bool applyConditions = true; - foreach (ProfileCondition applyCondition in i.ApplyConditions) - { - int? width = videoStream?.Width; - int? height = videoStream?.Height; - int? bitDepth = videoStream?.BitDepth; - int? videoBitrate = videoStream?.BitRate; - double? videoLevel = videoStream?.Level; - string videoProfile = videoStream?.Profile; - float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; - bool? isAnamorphic = videoStream?.IsAnamorphic; - bool? isInterlaced = videoStream?.IsInterlaced; - string videoCodecTag = videoStream?.CodecTag; - bool? isAvc = videoStream?.IsAVC; - - TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp; - int? packetLength = videoStream?.PacketLength; - int? refFrames = videoStream?.RefFrames; - - int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio); - int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video); - - if (!ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)) - { - // LogConditionFailure(options.Profile, "VideoCodecProfile.ApplyConditions", applyCondition, item); - applyConditions = false; - break; - } - } - - if (applyConditions) - { - var transcodingVideoCodecs = ContainerProfile.SplitValue(transcodingProfile.VideoCodec); - foreach (var transcodingVideoCodec in transcodingVideoCodecs) - { - if (i.ContainsAnyCodec(transcodingVideoCodec, transcodingProfile.Container)) - { - ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, isFirstAppliedCodecProfile); - isFirstAppliedCodecProfile = false; - } - } - } + ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, isFirstAppliedCodecProfile); + isFirstAppliedCodecProfile = false; + continue; } } + } - // Honor requested max channels - playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels; + // Honor requested max channels + playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels; - int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(false) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem); - playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate); + int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(false) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem); + playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate); - isFirstAppliedCodecProfile = true; - foreach (var i in options.Profile.CodecProfiles) + bool? isSecondaryAudio = audioStream == null ? null : item.IsSecondaryAudio(audioStream); + int? inputAudioBitrate = audioStream == null ? null : audioStream.BitRate; + int? audioChannels = audioStream == null ? null : audioStream.Channels; + string audioProfile = audioStream == null ? null : audioStream.Profile; + int? inputAudioSampleRate = audioStream == null ? null : audioStream.SampleRate; + int? inputAudioBitDepth = audioStream == null ? null : audioStream.BitDepth; + + var appliedAudioConditions = options.Profile.CodecProfiles + .Where(i => i.Type == CodecType.Video && + i.ContainsAnyCodec(audioCodec, container) && + i.ApplyConditions.Any(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio))); + isFirstAppliedCodecProfile = true; + foreach (var i in appliedAudioConditions) + { + var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec); + foreach (var transcodingAudioCodec in transcodingAudioCodecs) { - if (i.Type == CodecType.VideoAudio && i.ContainsAnyCodec(transcodingProfile.AudioCodec, transcodingProfile.Container)) + if (i.ContainsAnyCodec(transcodingAudioCodec, container)) { - bool applyConditions = true; - foreach (ProfileCondition applyCondition in i.ApplyConditions) - { - bool? isSecondaryAudio = audioStream == null ? null : item.IsSecondaryAudio(audioStream); - int? inputAudioBitrate = audioStream == null ? null : audioStream.BitRate; - int? audioChannels = audioStream == null ? null : audioStream.Channels; - string audioProfile = audioStream == null ? null : audioStream.Profile; - int? inputAudioSampleRate = audioStream == null ? null : audioStream.SampleRate; - int? inputAudioBitDepth = audioStream == null ? null : audioStream.BitDepth; - - if (!ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)) - { - // LogConditionFailure(options.Profile, "VideoCodecProfile.ApplyConditions", applyCondition, item); - applyConditions = false; - break; - } - } - - if (applyConditions) - { - var transcodingAudioCodecs = ContainerProfile.SplitValue(transcodingProfile.AudioCodec); - foreach (var transcodingAudioCodec in transcodingAudioCodecs) - { - if (i.ContainsAnyCodec(transcodingAudioCodec, transcodingProfile.Container)) - { - ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile); - isFirstAppliedCodecProfile = false; - } - } - } + ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile); + isFirstAppliedCodecProfile = false; + break; } } + } - var maxBitrateSetting = options.GetMaxBitrate(false); - // Honor max rate - if (maxBitrateSetting.HasValue) - { - var availableBitrateForVideo = maxBitrateSetting.Value; - - if (playlistItem.AudioBitrate.HasValue) - { - availableBitrateForVideo -= playlistItem.AudioBitrate.Value; - } + var maxBitrateSetting = options.GetMaxBitrate(false); + // Honor max rate + if (maxBitrateSetting.HasValue) + { + var availableBitrateForVideo = maxBitrateSetting.Value; - // Make sure the video bitrate is lower than bitrate settings but at least 64k - long currentValue = playlistItem.VideoBitrate ?? availableBitrateForVideo; - var longBitrate = Math.Max(Math.Min(availableBitrateForVideo, currentValue), 64000); - playlistItem.VideoBitrate = longBitrate >= int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate); + if (playlistItem.AudioBitrate.HasValue) + { + availableBitrateForVideo -= playlistItem.AudioBitrate.Value; } - } - playlistItem.TranscodeReasons = transcodeReasons.ToArray(); + // Make sure the video bitrate is lower than bitrate settings but at least 64k + // Don't use Math.Clamp as availableBitrateForVideo can be lower then 64k. + var currentValue = playlistItem.VideoBitrate ?? availableBitrateForVideo; + playlistItem.VideoBitrate = Math.Max(Math.Min(availableBitrateForVideo, currentValue), 64_000); + } - return playlistItem; + _logger.LogInformation( + "Transcode Result for Profile: {Profile}, Path: {Path}, PlayMethod: {PlayMethod}, AudioStreamIndex: {AudioStreamIndex}, SubtitleStreamIndex: {SubtitleStreamIndex}, Reasons: {TranscodeReason}", + options.Profile?.Name ?? "Anonymous Profile", + item.Path ?? "Unknown path", + playlistItem?.PlayMethod, + audioStream?.Index, + playlistItem?.SubtitleStreamIndex, + playlistItem?.TranscodeReasons); } private static int GetDefaultAudioBitrate(string audioCodec, int? audioChannels) @@ -1000,63 +1044,30 @@ namespace MediaBrowser.Model.Dlna return 7168000; } - private (PlayMethod? PlayMethod, List<TranscodeReason> TranscodeReasons) GetVideoDirectPlayProfile( + private (DirectPlayProfile Profile, PlayMethod? PlayMethod, int? AudioStreamIndex, TranscodeReason TranscodeReasons) GetVideoDirectPlayProfile( VideoOptions options, MediaSourceInfo mediaSource, MediaStream videoStream, MediaStream audioStream, + IEnumerable<MediaStream> candidateAudioStreams, + MediaStream subtitleStream, + bool isEligibleForDirectPlay, bool isEligibleForDirectStream) { if (options.ForceDirectPlay) { - return (PlayMethod.DirectPlay, new List<TranscodeReason>()); + return (null, PlayMethod.DirectPlay, audioStream?.Index, 0); } if (options.ForceDirectStream) { - return (PlayMethod.DirectStream, new List<TranscodeReason>()); + return (null, PlayMethod.DirectStream, audioStream?.Index, 0); } DeviceProfile profile = options.Profile; string container = mediaSource.Container; - // See if it can be direct played - DirectPlayProfile directPlay = null; - foreach (var p in profile.DirectPlayProfiles) - { - if (p.Type == DlnaProfileType.Video && IsVideoDirectPlaySupported(p, container, videoStream, audioStream)) - { - directPlay = p; - break; - } - } - - if (directPlay == null) - { - _logger.LogDebug( - "Container: {Container}, Video: {Video}, Audio: {Audio} cannot be direct played by profile: {Profile} for path: {Path}", - container, - videoStream?.Codec ?? "no video", - audioStream?.Codec ?? "no audio", - profile.Name ?? "unknown profile", - mediaSource.Path ?? "unknown path"); - - return (null, GetTranscodeReasonsFromDirectPlayProfile(mediaSource, videoStream, audioStream, profile.DirectPlayProfiles)); - } - - var conditions = new List<ProfileCondition>(); - foreach (var p in profile.ContainerProfiles) - { - if (p.Type == DlnaProfileType.Video - && p.ContainsContainer(container)) - { - foreach (var c in p.Conditions) - { - conditions.Add(c); - } - } - } - + // video int? width = videoStream?.Width; int? height = videoStream?.Height; int? bitDepth = videoStream?.BitDepth; @@ -1068,12 +1079,9 @@ namespace MediaBrowser.Model.Dlna bool? isInterlaced = videoStream?.IsInterlaced; string videoCodecTag = videoStream?.CodecTag; bool? isAvc = videoStream?.IsAVC; - - int? audioBitrate = audioStream?.BitRate; - int? audioChannels = audioStream?.Channels; - string audioProfile = audioStream?.Profile; - int? audioSampleRate = audioStream?.SampleRate; - int? audioBitDepth = audioStream?.BitDepth; + // audio + var defaultLanguage = audioStream?.Language ?? string.Empty; + var defaultMarked = audioStream?.IsDefault ?? false; TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : mediaSource.Timestamp; int? packetLength = videoStream?.PacketLength; @@ -1082,118 +1090,165 @@ namespace MediaBrowser.Model.Dlna int? numAudioStreams = mediaSource.GetStreamCount(MediaStreamType.Audio); int? numVideoStreams = mediaSource.GetStreamCount(MediaStreamType.Video); - // Check container conditions - foreach (ProfileCondition i in conditions) - { - if (!ConditionProcessor.IsVideoConditionSatisfied(i, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)) - { - LogConditionFailure(profile, "VideoContainerProfile", i, mediaSource); - - var transcodeReason = GetTranscodeReasonForFailedCondition(i); - var transcodeReasons = transcodeReason.HasValue - ? new List<TranscodeReason> { transcodeReason.Value } - : new List<TranscodeReason>(); + var checkVideoConditions = (ProfileCondition[] conditions) => + conditions.Where(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)); - return (null, transcodeReasons); - } - } - - string videoCodec = videoStream?.Codec; - - conditions = new List<ProfileCondition>(); - foreach (var i in profile.CodecProfiles) - { - if (i.Type == CodecType.Video && i.ContainsAnyCodec(videoCodec, container)) - { - bool applyConditions = true; - foreach (ProfileCondition applyCondition in i.ApplyConditions) + // Check container conditions + var containerProfileReasons = AggregateFailureConditions( + mediaSource, + profile, + "VideoCodecProfile", + profile.ContainerProfiles + .Where(containerProfile => containerProfile.Type == DlnaProfileType.Video && containerProfile.ContainsContainer(container)) + .SelectMany(containerProfile => checkVideoConditions(containerProfile.Conditions))); + + // Check video conditions + var videoCodecProfileReasons = AggregateFailureConditions( + mediaSource, + profile, + "VideoCodecProfile", + profile.CodecProfiles + .Where(codecProfile => codecProfile.Type == CodecType.Video && codecProfile.ContainsAnyCodec(videoStream?.Codec, container)) + .SelectMany(codecProfile => { - if (!ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)) + var failedApplyConditions = checkVideoConditions(codecProfile.ApplyConditions); + if (!failedApplyConditions.Any()) { - // LogConditionFailure(profile, "VideoCodecProfile.ApplyConditions", applyCondition, mediaSource); - applyConditions = false; - break; + return Array.Empty<ProfileCondition>(); } - } - if (applyConditions) - { - foreach (ProfileCondition c in i.Conditions) - { - conditions.Add(c); - } - } - } - } + var failedConditions = checkVideoConditions(codecProfile.Conditions); + return failedApplyConditions.Concat(failedConditions); + })); - foreach (ProfileCondition i in conditions) - { - if (!ConditionProcessor.IsVideoConditionSatisfied(i, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)) - { - LogConditionFailure(profile, "VideoCodecProfile", i, mediaSource); + // Check audiocandidates profile conditions + var audioStreamMatches = candidateAudioStreams.ToDictionary(s => s, audioStream => CheckVideoAudioStreamDirectPlay(options, mediaSource, container, audioStream, defaultLanguage, defaultMarked)); - var transcodeReason = GetTranscodeReasonForFailedCondition(i); - var transcodeReasons = transcodeReason.HasValue - ? new List<TranscodeReason> { transcodeReason.Value } - : new List<TranscodeReason>(); + TranscodeReason subtitleProfileReasons = 0; + if (subtitleStream != null) + { + var subtitleProfile = GetSubtitleProfile(mediaSource, subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.DirectPlay, _transcoderSupport, container, null); - return (null, transcodeReasons); + if (subtitleProfile.Method != SubtitleDeliveryMethod.Drop + && subtitleProfile.Method != SubtitleDeliveryMethod.External + && subtitleProfile.Method != SubtitleDeliveryMethod.Embed) + { + _logger.LogDebug("Not eligible for {0} due to unsupported subtitles", PlayMethod.DirectPlay); + subtitleProfileReasons |= TranscodeReason.SubtitleCodecNotSupported; } } - if (audioStream != null) - { - string audioCodec = audioStream.Codec; - conditions = new List<ProfileCondition>(); - bool? isSecondaryAudio = mediaSource.IsSecondaryAudio(audioStream); - - foreach (var i in profile.CodecProfiles) + var rankings = new[] { VideoReasons, AudioReasons, ContainerReasons }; + var rank = (ref TranscodeReason a) => { - if (i.Type == CodecType.VideoAudio && i.ContainsAnyCodec(audioCodec, container)) + var index = 1; + foreach (var flag in rankings) { - bool applyConditions = true; - foreach (ProfileCondition applyCondition in i.ApplyConditions) + var reason = a & flag; + if (reason != 0) { - if (!ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio)) - { - // LogConditionFailure(profile, "VideoAudioCodecProfile.ApplyConditions", applyCondition, mediaSource); - applyConditions = false; - break; - } + a = reason; + return index; } - if (applyConditions) - { - foreach (ProfileCondition c in i.Conditions) - { - conditions.Add(c); - } - } + index++; } - } - foreach (ProfileCondition i in conditions) + return index; + }; + + // Check DirectPlay profiles to see if it can be direct played + var analyzedProfiles = profile.DirectPlayProfiles + .Where(directPlayProfile => directPlayProfile.Type == DlnaProfileType.Video) + .Select((directPlayProfile, order) => { - if (!ConditionProcessor.IsVideoAudioConditionSatisfied(i, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio)) + TranscodeReason directPlayProfileReasons = 0; + TranscodeReason audioCodecProfileReasons = 0; + + // Check container type + if (!directPlayProfile.SupportsContainer(container)) { - LogConditionFailure(profile, "VideoAudioCodecProfile", i, mediaSource); + directPlayProfileReasons |= TranscodeReason.ContainerNotSupported; + } - var transcodeReason = GetTranscodeReasonForFailedCondition(i); - var transcodeReasons = transcodeReason.HasValue - ? new List<TranscodeReason> { transcodeReason.Value } - : new List<TranscodeReason>(); + // Check video codec + string videoCodec = videoStream?.Codec; + if (!directPlayProfile.SupportsVideoCodec(videoCodec)) + { + directPlayProfileReasons |= TranscodeReason.VideoCodecNotSupported; + } - return (null, transcodeReasons); + // Check audio codec + var selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.SupportsAudioCodec(audioStream.Codec)); + if (selectedAudioStream == null) + { + directPlayProfileReasons |= TranscodeReason.AudioCodecNotSupported; } - } + else + { + audioCodecProfileReasons = audioStreamMatches.GetValueOrDefault(selectedAudioStream); + } + + var failureReasons = directPlayProfileReasons | containerProfileReasons | videoCodecProfileReasons | audioCodecProfileReasons | subtitleProfileReasons; + var directStreamFailureReasons = failureReasons & (~DirectStreamReasons); + + PlayMethod? playMethod = null; + if (failureReasons == 0 && isEligibleForDirectPlay && mediaSource.SupportsDirectPlay) + { + playMethod = PlayMethod.DirectPlay; + } + else if (directStreamFailureReasons == 0 && isEligibleForDirectStream && mediaSource.SupportsDirectStream && directPlayProfile != null) + { + playMethod = PlayMethod.DirectStream; + } + + var ranked = rank(ref failureReasons); + return (Result: (Profile: directPlayProfile, PlayMethod: playMethod, AudioStreamIndex: selectedAudioStream?.Index, TranscodeReason: failureReasons), Order: order, Rank: ranked); + }) + .OrderByDescending(analysis => analysis.Result.PlayMethod) + .ThenBy(analysis => analysis.Order) + .ToArray() + .ToLookup(analysis => analysis.Result.PlayMethod != null); + + var profileMatch = analyzedProfiles[true] + .Select(analysis => analysis.Result) + .FirstOrDefault(); + if (profileMatch.Profile != null) + { + return profileMatch; + } + + var failureReasons = analyzedProfiles[false].OrderBy(a => a.Result.TranscodeReason).ThenBy(analysis => analysis.Order).FirstOrDefault().Result.TranscodeReason; + if (failureReasons == 0) + { + failureReasons = TranscodeReason.DirectPlayError; } - if (isEligibleForDirectStream && mediaSource.SupportsDirectStream) + return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: failureReasons); + } + + private TranscodeReason CheckVideoAudioStreamDirectPlay(VideoOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream, string language, bool isDefault) + { + var profile = options.Profile; + var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, !audioStream.IsDefault); + + var audioStreamFailureReasons = AggregateFailureConditions(mediaSource, profile, "VideoAudioCodecProfile", audioFailureConditions); + if (audioStream?.IsExternal == true) { - return (PlayMethod.DirectStream, new List<TranscodeReason>()); + audioStreamFailureReasons |= TranscodeReason.AudioIsExternal; } - return (null, new List<TranscodeReason> { TranscodeReason.ContainerBitrateExceedsLimit }); + return audioStreamFailureReasons; + } + + private TranscodeReason AggregateFailureConditions(MediaSourceInfo mediaSource, DeviceProfile profile, string type, IEnumerable<ProfileCondition> conditions) + { + return conditions.Aggregate<ProfileCondition, TranscodeReason>(0, (reasons, i) => + { + LogConditionFailure(profile, type, i, mediaSource); + var transcodeReasons = GetTranscodeReasonForFailedCondition(i); + return reasons | transcodeReasons; + }); } private void LogConditionFailure(DeviceProfile profile, string type, ProfileCondition condition, MediaSourceInfo mediaSource) @@ -1209,39 +1264,21 @@ namespace MediaBrowser.Model.Dlna mediaSource.Path ?? "Unknown path"); } - private (bool DirectPlay, TranscodeReason? Reason) IsEligibleForDirectPlay( + private TranscodeReason IsEligibleForDirectPlay( MediaSourceInfo item, long maxBitrate, - MediaStream subtitleStream, - MediaStream audioStream, VideoOptions options, PlayMethod playMethod) { - if (subtitleStream != null) - { - var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, playMethod, _transcoderSupport, item.Container, null); - - if (subtitleProfile.Method != SubtitleDeliveryMethod.Drop - && subtitleProfile.Method != SubtitleDeliveryMethod.External - && subtitleProfile.Method != SubtitleDeliveryMethod.Embed) - { - _logger.LogDebug("Not eligible for {0} due to unsupported subtitles", playMethod); - return (false, TranscodeReason.SubtitleCodecNotSupported); - } - } - - bool result = IsAudioEligibleForDirectPlay(item, maxBitrate, playMethod); + bool result = IsItemBitrateEligibleForDirectPlay(item, maxBitrate, playMethod); if (!result) { - return (false, TranscodeReason.ContainerBitrateExceedsLimit); + return TranscodeReason.ContainerBitrateExceedsLimit; } - - if (audioStream?.IsExternal == true) + else { - return (false, TranscodeReason.AudioIsExternal); + return 0; } - - return (true, null); } public static SubtitleProfile GetSubtitleProfile( @@ -1401,7 +1438,7 @@ namespace MediaBrowser.Model.Dlna return null; } - private bool IsAudioEligibleForDirectPlay(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod) + private bool IsItemBitrateEligibleForDirectPlay(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod) { // Don't restrict by bitrate if coming from an external domain if (item.IsRemote) @@ -1444,7 +1481,7 @@ namespace MediaBrowser.Model.Dlna private static void ValidateAudioInput(AudioOptions options) { - if (options.ItemId.Equals(Guid.Empty)) + if (options.ItemId.Equals(default)) { throw new ArgumentException("ItemId is required"); } @@ -1465,6 +1502,47 @@ namespace MediaBrowser.Model.Dlna } } + private static IEnumerable<ProfileCondition> GetProfileConditionsForVideoAudio( + IEnumerable<CodecProfile> codecProfiles, + string container, + string codec, + int? audioChannels, + int? audioBitrate, + int? audioSampleRate, + int? audioBitDepth, + string audioProfile, + bool? isSecondaryAudio) + { + return codecProfiles + .Where(profile => profile.Type == CodecType.VideoAudio && profile.ContainsAnyCodec(codec, container) && + profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio))) + .SelectMany(profile => profile.Conditions) + .Where(condition => !ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio)); + } + + private static IEnumerable<ProfileCondition> GetProfileConditionsForAudio( + IEnumerable<CodecProfile> codecProfiles, + string container, + string codec, + int? audioChannels, + int? audioBitrate, + int? audioSampleRate, + int? audioBitDepth, + bool checkConditions) + { + var conditions = codecProfiles + .Where(profile => profile.Type == CodecType.Audio && profile.ContainsAnyCodec(codec, container) && + profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth))) + .SelectMany(profile => profile.Conditions); + + if (!checkConditions) + { + return conditions; + } + + return conditions.Where(condition => !ConditionProcessor.IsAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth)); + } + private void ApplyTranscodingConditions(StreamInfo item, IEnumerable<ProfileCondition> conditions, string qualifier, bool enableQualifiedConditions, bool enableNonQualifiedConditions) { foreach (ProfileCondition condition in conditions) @@ -1744,10 +1822,22 @@ namespace MediaBrowser.Model.Dlna var values = value .Split('|', StringSplitOptions.RemoveEmptyEntries); - if (condition.Condition == ProfileConditionType.Equals || condition.Condition == ProfileConditionType.EqualsAny) + if (condition.Condition == ProfileConditionType.Equals) { item.SetOption(qualifier, "profile", string.Join(',', values)); } + else if (condition.Condition == ProfileConditionType.EqualsAny) + { + var currentValue = item.GetOption(qualifier, "profile"); + if (!string.IsNullOrEmpty(currentValue) && values.Any(value => value == currentValue)) + { + item.SetOption(qualifier, "profile", currentValue); + } + else + { + item.SetOption(qualifier, "profile", string.Join(',', values)); + } + } break; } @@ -1905,29 +1995,5 @@ namespace MediaBrowser.Model.Dlna return true; } - - private bool IsVideoDirectPlaySupported(DirectPlayProfile profile, string container, MediaStream videoStream, MediaStream audioStream) - { - // Check container type - if (!profile.SupportsContainer(container)) - { - return false; - } - - // Check video codec - string videoCodec = videoStream?.Codec; - if (!profile.SupportsVideoCodec(videoCodec)) - { - return false; - } - - // Check audio codec - if (audioStream != null && !profile.SupportsAudioCodec(audioStream.Codec)) - { - return false; - } - - return true; - } } } diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index a678c54e7..79dfff5c2 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -23,7 +23,6 @@ namespace MediaBrowser.Model.Dlna AudioCodecs = Array.Empty<string>(); VideoCodecs = Array.Empty<string>(); SubtitleCodecs = Array.Empty<string>(); - TranscodeReasons = Array.Empty<TranscodeReason>(); StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } @@ -103,7 +102,7 @@ namespace MediaBrowser.Model.Dlna public string PlaySessionId { get; set; } - public TranscodeReason[] TranscodeReasons { get; set; } + public TranscodeReason TranscodeReasons { get; set; } public Dictionary<string, string> StreamOptions { get; private set; } @@ -799,7 +798,7 @@ namespace MediaBrowser.Model.Dlna if (!item.IsDirectStream) { - list.Add(new NameValuePair("TranscodeReasons", string.Join(',', item.TranscodeReasons.Distinct()))); + list.Add(new NameValuePair("TranscodeReasons", item.TranscodeReasons.ToString())); } return list; diff --git a/MediaBrowser.Model/Dlna/TranscodingProfile.cs b/MediaBrowser.Model/Dlna/TranscodingProfile.cs index 709bdad31..b4f6ec255 100644 --- a/MediaBrowser.Model/Dlna/TranscodingProfile.cs +++ b/MediaBrowser.Model/Dlna/TranscodingProfile.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using System; using System.ComponentModel; using System.Xml.Serialization; @@ -7,6 +8,11 @@ namespace MediaBrowser.Model.Dlna { public class TranscodingProfile { + public TranscodingProfile() + { + Conditions = Array.Empty<ProfileCondition>(); + } + [XmlAttribute("container")] public string Container { get; set; } = string.Empty; @@ -61,6 +67,8 @@ namespace MediaBrowser.Model.Dlna [XmlAttribute("breakOnNonKeyFrames")] public bool BreakOnNonKeyFrames { get; set; } + public ProfileCondition[] Conditions { get; set; } + public string[] GetAudioCodecs() { return ContainerProfile.SplitValue(AudioCodec); diff --git a/MediaBrowser.Model/Dlna/VideoOptions.cs b/MediaBrowser.Model/Dlna/VideoOptions.cs index 4194f17c6..0cb80af54 100644 --- a/MediaBrowser.Model/Dlna/VideoOptions.cs +++ b/MediaBrowser.Model/Dlna/VideoOptions.cs @@ -10,5 +10,7 @@ namespace MediaBrowser.Model.Dlna public int? AudioStreamIndex { get; set; } public int? SubtitleStreamIndex { get; set; } + + public bool AllowVideoStreamCopy { get; set; } } } diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs index 049e14333..bb9848848 100644 --- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs +++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs @@ -109,7 +109,7 @@ namespace MediaBrowser.Model.Dto public int? AnalyzeDurationMs { get; set; } [JsonIgnore] - public TranscodeReason[] TranscodeReasons { get; set; } + public TranscodeReason TranscodeReasons { get; set; } public int? DefaultAudioStreamIndex { get; set; } @@ -161,7 +161,7 @@ namespace MediaBrowser.Model.Dto public MediaStream GetDefaultAudioStream(int? defaultIndex) { - if (defaultIndex.HasValue) + if (defaultIndex.HasValue && defaultIndex != -1) { var val = defaultIndex.Value; diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 38ac44794..4742d21e9 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; +using Jellyfin.Extensions; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.MediaInfo; @@ -17,6 +18,18 @@ namespace MediaBrowser.Model.Entities /// </summary> public class MediaStream { + private static readonly string[] _specialCodes = + { + // Uncoded languages. + "mis", + // Multiple languages. + "mul", + // Undetermined. + "und", + // No linguistic content; not applicable. + "zxx" + }; + /// <summary> /// Gets or sets the codec. /// </summary> @@ -127,6 +140,8 @@ namespace MediaBrowser.Model.Entities public string LocalizedForced { get; set; } + public string LocalizedExternal { get; set; } + public string DisplayTitle { get @@ -137,7 +152,8 @@ namespace MediaBrowser.Model.Entities { var attributes = new List<string>(); - if (!string.IsNullOrEmpty(Language)) + // Do not display the language code in display titles if unset or set to a special code. Show it in all other cases (possibly expanded). + if (!string.IsNullOrEmpty(Language) && !_specialCodes.Contains(Language, StringComparison.OrdinalIgnoreCase)) { // Get full language string i.e. eng -> English. Will not work for some languages which use ISO 639-2/B instead of /T codes. string fullLanguage = CultureInfo @@ -147,7 +163,7 @@ namespace MediaBrowser.Model.Entities attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language)); } - if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase) && !string.Equals(Codec, "dts", StringComparison.OrdinalIgnoreCase)) { attributes.Add(AudioCodec.GetFriendlyName(Codec)); } @@ -170,6 +186,11 @@ namespace MediaBrowser.Model.Entities attributes.Add(string.IsNullOrEmpty(LocalizedDefault) ? "Default" : LocalizedDefault); } + if (IsExternal) + { + attributes.Add(string.IsNullOrEmpty(LocalizedExternal) ? "External" : LocalizedExternal); + } + if (!string.IsNullOrEmpty(Title)) { var result = new StringBuilder(Title); @@ -260,6 +281,11 @@ namespace MediaBrowser.Model.Entities attributes.Add(Codec.ToUpperInvariant()); } + if (IsExternal) + { + attributes.Add(string.IsNullOrEmpty(LocalizedExternal) ? "External" : LocalizedExternal); + } + if (!string.IsNullOrEmpty(Title)) { var result = new StringBuilder(Title); diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs index 7207795b0..786b20e9e 100644 --- a/MediaBrowser.Model/IO/IFileSystem.cs +++ b/MediaBrowser.Model/IO/IFileSystem.cs @@ -200,5 +200,19 @@ namespace MediaBrowser.Model.IO void SetAttributes(string path, bool isHidden, bool readOnly); IEnumerable<FileSystemMetadata> GetDrives(); + + /// <summary> + /// Determines whether the directory exists. + /// </summary> + /// <param name="path">The path.</param> + /// <returns>Whether the path exists.</returns> + bool DirectoryExists(string path); + + /// <summary> + /// Determines whether the file exists. + /// </summary> + /// <param name="path">The path.</param> + /// <returns>Whether the path exists.</returns> + bool FileExists(string path); } } diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 4386f75af..43b465b29 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -34,7 +34,7 @@ <ItemGroup> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" /> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" /> <PackageReference Include="MimeTypes" Version="2.3.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> @@ -54,7 +54,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.Model/Net/ISocketFactory.cs b/MediaBrowser.Model/Net/ISocketFactory.cs index 1527ef595..a2835b711 100644 --- a/MediaBrowser.Model/Net/ISocketFactory.cs +++ b/MediaBrowser.Model/Net/ISocketFactory.cs @@ -26,6 +26,6 @@ namespace MediaBrowser.Model.Net /// <param name="multicastTimeToLive">The multicast time to live value. Actually a maximum number of network hops for UDP packets.</param> /// <param name="localPort">The local port to bind to.</param> /// <returns>A <see cref="ISocket"/> implementation.</returns> - ISocket CreateUdpMulticastSocket(string ipAddress, int multicastTimeToLive, int localPort); + ISocket CreateUdpMulticastSocket(IPAddress ipAddress, int multicastTimeToLive, int localPort); } } diff --git a/MediaBrowser.Model/Querying/ItemSortBy.cs b/MediaBrowser.Model/Querying/ItemSortBy.cs index 0b846bb96..0a28acf37 100644 --- a/MediaBrowser.Model/Querying/ItemSortBy.cs +++ b/MediaBrowser.Model/Querying/ItemSortBy.cs @@ -102,5 +102,9 @@ namespace MediaBrowser.Model.Querying public const string DateLastContentAdded = "DateLastContentAdded"; public const string SeriesDatePlayed = "SeriesDatePlayed"; + + public const string ParentIndexNumber = "ParentIndexNumber"; + + public const string IndexNumber = "IndexNumber"; } } diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs index fa8aa829d..133d6a916 100644 --- a/MediaBrowser.Model/Querying/NextUpQuery.cs +++ b/MediaBrowser.Model/Querying/NextUpQuery.cs @@ -14,6 +14,7 @@ namespace MediaBrowser.Model.Querying EnableTotalRecordCount = true; DisableFirstEpisode = false; NextUpDateCutoff = DateTime.MinValue; + EnableRewatching = false; } /// <summary> @@ -81,5 +82,10 @@ namespace MediaBrowser.Model.Querying /// Gets or sets a value indicating the oldest date for a show to appear in Next Up. /// </summary> public DateTime NextUpDateCutoff { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether getting rewatching next up list. + /// </summary> + public bool EnableRewatching { get; set; } } } diff --git a/MediaBrowser.Model/Session/GeneralCommand.cs b/MediaBrowser.Model/Session/GeneralCommand.cs index 29528c110..757b19b31 100644 --- a/MediaBrowser.Model/Session/GeneralCommand.cs +++ b/MediaBrowser.Model/Session/GeneralCommand.cs @@ -2,20 +2,26 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; -namespace MediaBrowser.Model.Session +namespace MediaBrowser.Model.Session; + +public class GeneralCommand { - public class GeneralCommand + public GeneralCommand() + : this(new Dictionary<string, string>()) + { + } + + [JsonConstructor] + public GeneralCommand(Dictionary<string, string> arguments) { - public GeneralCommand() - { - Arguments = new Dictionary<string, string>(); - } + Arguments = arguments; + } - public GeneralCommandType Name { get; set; } + public GeneralCommandType Name { get; set; } - public Guid ControllingUserId { get; set; } + public Guid ControllingUserId { get; set; } - public Dictionary<string, string> Arguments { get; } - } + public Dictionary<string, string> Arguments { get; } } diff --git a/MediaBrowser.Model/Session/HardwareEncodingType.cs b/MediaBrowser.Model/Session/HardwareEncodingType.cs index 0db5697d3..f5753467a 100644 --- a/MediaBrowser.Model/Session/HardwareEncodingType.cs +++ b/MediaBrowser.Model/Session/HardwareEncodingType.cs @@ -21,28 +21,18 @@ NVENC = 2, /// <summary> - /// OpenMax OMX. + /// Video4Linux2 V4L2. /// </summary> - OMX = 3, - - /// <summary> - /// Exynos V4L2 MFC. - /// </summary> - V4L2M2M = 4, - - /// <summary> - /// MediaCodec Android. - /// </summary> - MediaCodec = 5, + V4L2M2M = 3, /// <summary> /// Video Acceleration API (VAAPI). /// </summary> - VAAPI = 6, + VAAPI = 4, /// <summary> /// Video ToolBox. /// </summary> - VideoToolBox = 7 + VideoToolBox = 5 } } diff --git a/MediaBrowser.Model/Session/TranscodeReason.cs b/MediaBrowser.Model/Session/TranscodeReason.cs index 3c95df66d..9da9f3323 100644 --- a/MediaBrowser.Model/Session/TranscodeReason.cs +++ b/MediaBrowser.Model/Session/TranscodeReason.cs @@ -1,32 +1,44 @@ #pragma warning disable CS1591 +using System; + namespace MediaBrowser.Model.Session { + [Flags] public enum TranscodeReason { - ContainerNotSupported = 0, - VideoCodecNotSupported = 1, - AudioCodecNotSupported = 2, - ContainerBitrateExceedsLimit = 3, - AudioBitrateNotSupported = 4, - AudioChannelsNotSupported = 5, - VideoResolutionNotSupported = 6, - UnknownVideoStreamInfo = 7, - UnknownAudioStreamInfo = 8, - AudioProfileNotSupported = 9, - AudioSampleRateNotSupported = 10, - AnamorphicVideoNotSupported = 11, - InterlacedVideoNotSupported = 12, - SecondaryAudioNotSupported = 13, - RefFramesNotSupported = 14, - VideoBitDepthNotSupported = 15, - VideoBitrateNotSupported = 16, - VideoFramerateNotSupported = 17, - VideoLevelNotSupported = 18, - VideoProfileNotSupported = 19, - AudioBitDepthNotSupported = 20, - SubtitleCodecNotSupported = 21, - DirectPlayError = 22, - AudioIsExternal = 23 + // Primary + ContainerNotSupported = 1 << 0, + VideoCodecNotSupported = 1 << 1, + AudioCodecNotSupported = 1 << 2, + SubtitleCodecNotSupported = 1 << 3, + AudioIsExternal = 1 << 4, + SecondaryAudioNotSupported = 1 << 5, + + // Video Constraints + VideoProfileNotSupported = 1 << 6, + VideoLevelNotSupported = 1 << 7, + VideoResolutionNotSupported = 1 << 8, + VideoBitDepthNotSupported = 1 << 9, + VideoFramerateNotSupported = 1 << 10, + RefFramesNotSupported = 1 << 11, + AnamorphicVideoNotSupported = 1 << 12, + InterlacedVideoNotSupported = 1 << 13, + + // Audio Constraints + AudioChannelsNotSupported = 1 << 14, + AudioProfileNotSupported = 1 << 15, + AudioSampleRateNotSupported = 1 << 16, + AudioBitDepthNotSupported = 1 << 17, + + // Bitrate Constraints + ContainerBitrateExceedsLimit = 1 << 18, + VideoBitrateNotSupported = 1 << 19, + AudioBitrateNotSupported = 1 << 20, + + // Errors + UnknownVideoStreamInfo = 1 << 21, + UnknownAudioStreamInfo = 1 << 22, + DirectPlayError = 1 << 23, } } diff --git a/MediaBrowser.Model/Session/TranscodingInfo.cs b/MediaBrowser.Model/Session/TranscodingInfo.cs index 68ab691f8..f876fa961 100644 --- a/MediaBrowser.Model/Session/TranscodingInfo.cs +++ b/MediaBrowser.Model/Session/TranscodingInfo.cs @@ -1,17 +1,10 @@ #nullable disable #pragma warning disable CS1591 -using System; - namespace MediaBrowser.Model.Session { public class TranscodingInfo { - public TranscodingInfo() - { - TranscodeReasons = Array.Empty<TranscodeReason>(); - } - public string AudioCodec { get; set; } public string VideoCodec { get; set; } @@ -36,6 +29,6 @@ namespace MediaBrowser.Model.Session public HardwareEncodingType? HardwareAccelerationType { get; set; } - public TranscodeReason[] TranscodeReasons { get; set; } + public TranscodeReason TranscodeReason { get; set; } } } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 5281b3721..0c31d460f 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -412,7 +412,7 @@ namespace MediaBrowser.Providers.Manager } // If this restriction is ever lifted, movie xml providers will have to be updated to prevent owned items like trailers from reading those files - if (!item.OwnerId.Equals(Guid.Empty)) + if (!item.OwnerId.Equals(default)) { if (provider is ILocalMetadataProvider || provider is IRemoteMetadataProvider) { @@ -781,7 +781,7 @@ namespace MediaBrowser.Providers.Manager { BaseItem referenceItem = null; - if (!searchInfo.ItemId.Equals(Guid.Empty)) + if (!searchInfo.ItemId.Equals(default)) { referenceItem = _libraryManager.GetItemById(searchInfo.ItemId); } diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 1851a9e4b..535f1b16f 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -43,7 +43,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/MediaBrowser.Providers/MediaInfo/AudioResolver.cs b/MediaBrowser.Providers/MediaInfo/AudioResolver.cs index 425913501..0bdf447ba 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioResolver.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioResolver.cs @@ -1,176 +1,36 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using Emby.Naming.Audio; using Emby.Naming.Common; -using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.IO; namespace MediaBrowser.Providers.MediaInfo { /// <summary> - /// Resolves external audios for videos. + /// Resolves external audio files for <see cref="Video"/>. /// </summary> - public class AudioResolver + public class AudioResolver : MediaInfoResolver { - private readonly ILocalizationManager _localizationManager; - private readonly IMediaEncoder _mediaEncoder; - private readonly NamingOptions _namingOptions; - /// <summary> - /// Initializes a new instance of the <see cref="AudioResolver"/> class. + /// Initializes a new instance of the <see cref="AudioResolver"/> class for external audio file processing. /// </summary> /// <param name="localizationManager">The localization manager.</param> /// <param name="mediaEncoder">The media encoder.</param> - /// <param name="namingOptions">The naming options.</param> + /// <param name="fileSystem">The file system.</param> + /// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param> public AudioResolver( ILocalizationManager localizationManager, IMediaEncoder mediaEncoder, + IFileSystem fileSystem, NamingOptions namingOptions) + : base( + localizationManager, + mediaEncoder, + fileSystem, + namingOptions, + DlnaProfileType.Audio) { - _localizationManager = localizationManager; - _mediaEncoder = mediaEncoder; - _namingOptions = namingOptions; - } - - /// <summary> - /// Returns the audio streams found in the external audio files for the given video. - /// </summary> - /// <param name="video">The video to get the external audio streams from.</param> - /// <param name="startIndex">The stream index to start adding audio streams at.</param> - /// <param name="directoryService">The directory service to search for files.</param> - /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param> - /// <param name="cancellationToken">The cancellation token to cancel operation.</param> - /// <returns>A list of external audio streams.</returns> - public async IAsyncEnumerable<MediaStream> GetExternalAudioStreams( - Video video, - int startIndex, - IDirectoryService directoryService, - bool clearCache, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (!video.IsFileProtocol) - { - yield break; - } - - IEnumerable<string> paths = GetExternalAudioFiles(video, directoryService, clearCache); - foreach (string path in paths) - { - string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path); - Model.MediaInfo.MediaInfo mediaInfo = await GetMediaInfo(path, cancellationToken).ConfigureAwait(false); - - foreach (MediaStream mediaStream in mediaInfo.MediaStreams) - { - mediaStream.Index = startIndex++; - mediaStream.Type = MediaStreamType.Audio; - mediaStream.IsExternal = true; - mediaStream.Path = path; - mediaStream.IsDefault = false; - mediaStream.Title = null; - - if (string.IsNullOrEmpty(mediaStream.Language)) - { - // Try to translate to three character code - // Be flexible and check against both the full and three character versions - var language = StringExtensions.RightPart(fileNameWithoutExtension, '.').ToString(); - - if (language != fileNameWithoutExtension) - { - var culture = _localizationManager.FindLanguageInfo(language); - - language = culture == null ? language : culture.ThreeLetterISOLanguageName; - mediaStream.Language = language; - } - } - - yield return mediaStream; - } - } - } - - /// <summary> - /// Returns the external audio file paths for the given video. - /// </summary> - /// <param name="video">The video to get the external audio file paths from.</param> - /// <param name="directoryService">The directory service to search for files.</param> - /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param> - /// <returns>A list of external audio file paths.</returns> - public IEnumerable<string> GetExternalAudioFiles( - Video video, - IDirectoryService directoryService, - bool clearCache) - { - if (!video.IsFileProtocol) - { - yield break; - } - - // Check if video folder exists - string folder = video.ContainingFolderPath; - if (!Directory.Exists(folder)) - { - yield break; - } - - string videoFileNameWithoutExtension = Path.GetFileNameWithoutExtension(video.Path); - - var files = directoryService.GetFilePaths(folder, clearCache, true); - for (int i = 0; i < files.Count; i++) - { - string file = files[i]; - if (string.Equals(video.Path, file, StringComparison.OrdinalIgnoreCase) - || !AudioFileParser.IsAudioFile(file, _namingOptions) - || Path.GetExtension(file.AsSpan()).Equals(".strm", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file); - // The audio filename must either be equal to the video filename or start with the video filename followed by a dot - if (videoFileNameWithoutExtension.Equals(fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase) - || (fileNameWithoutExtension.Length > videoFileNameWithoutExtension.Length - && fileNameWithoutExtension[videoFileNameWithoutExtension.Length] == '.' - && fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase))) - { - yield return file; - } - } - } - - /// <summary> - /// Returns the media info of the given audio file. - /// </summary> - /// <param name="path">The path to the audio file.</param> - /// <param name="cancellationToken">The cancellation token to cancel operation.</param> - /// <returns>The media info for the given audio file.</returns> - private Task<Model.MediaInfo.MediaInfo> GetMediaInfo(string path, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - return _mediaEncoder.GetMediaInfo( - new MediaInfoRequest - { - MediaType = DlnaProfileType.Audio, - MediaSource = new MediaSourceInfo - { - Path = path, - Protocol = MediaProtocol.File - } - }, - cancellationToken); } } } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs index 19a435196..fcd3f28d4 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs @@ -21,6 +21,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; @@ -39,11 +40,10 @@ namespace MediaBrowser.Providers.MediaInfo IHasItemChangeMonitor { private readonly ILogger<FFProbeProvider> _logger; - private readonly SubtitleResolver _subtitleResolver; private readonly AudioResolver _audioResolver; + private readonly SubtitleResolver _subtitleResolver; private readonly FFProbeVideoInfo _videoProber; private readonly FFProbeAudioInfo _audioProber; - private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None); public FFProbeProvider( @@ -58,11 +58,12 @@ namespace MediaBrowser.Providers.MediaInfo ISubtitleManager subtitleManager, IChapterManager chapterManager, ILibraryManager libraryManager, + IFileSystem fileSystem, NamingOptions namingOptions) { _logger = logger; - _audioResolver = new AudioResolver(localization, mediaEncoder, namingOptions); - _subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager); + _audioResolver = new AudioResolver(localization, mediaEncoder, fileSystem, namingOptions); + _subtitleResolver = new SubtitleResolver(localization, mediaEncoder, fileSystem, namingOptions); _videoProber = new FFProbeVideoInfo( _logger, mediaSourceManager, @@ -75,7 +76,8 @@ namespace MediaBrowser.Providers.MediaInfo subtitleManager, chapterManager, libraryManager, - _audioResolver); + _audioResolver, + _subtitleResolver); _audioProber = new FFProbeAudioInfo(mediaSourceManager, mediaEncoder, itemRepo, libraryManager); } @@ -104,7 +106,9 @@ namespace MediaBrowser.Providers.MediaInfo if (item.SupportsLocalMetadata && video != null && !video.IsPlaceHolder && !video.SubtitleFiles.SequenceEqual( - _subtitleResolver.GetExternalSubtitleFiles(video, directoryService, false), StringComparer.Ordinal)) + _subtitleResolver.GetExternalFiles(video, directoryService, false) + .Select(info => info.Path).ToList(), + StringComparer.Ordinal)) { _logger.LogDebug("Refreshing {ItemPath} due to external subtitles change.", item.Path); return true; @@ -112,7 +116,9 @@ namespace MediaBrowser.Providers.MediaInfo if (item.SupportsLocalMetadata && video != null && !video.IsPlaceHolder && !video.AudioFiles.SequenceEqual( - _audioResolver.GetExternalAudioFiles(video, directoryService, false), StringComparer.Ordinal)) + _audioResolver.GetExternalFiles(video, directoryService, false) + .Select(info => info.Path).ToList(), + StringComparer.Ordinal)) { _logger.LogDebug("Refreshing {ItemPath} due to external audio change.", item.Path); return true; diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 77a849d00..4a289b3ab 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -45,6 +45,7 @@ namespace MediaBrowser.Providers.MediaInfo private readonly IChapterManager _chapterManager; private readonly ILibraryManager _libraryManager; private readonly AudioResolver _audioResolver; + private readonly SubtitleResolver _subtitleResolver; private readonly IMediaSourceManager _mediaSourceManager; private readonly long _dummyChapterDuration = TimeSpan.FromMinutes(5).Ticks; @@ -61,9 +62,11 @@ namespace MediaBrowser.Providers.MediaInfo ISubtitleManager subtitleManager, IChapterManager chapterManager, ILibraryManager libraryManager, - AudioResolver audioResolver) + AudioResolver audioResolver, + SubtitleResolver subtitleResolver) { _logger = logger; + _mediaSourceManager = mediaSourceManager; _mediaEncoder = mediaEncoder; _itemRepo = itemRepo; _blurayExaminer = blurayExaminer; @@ -74,7 +77,7 @@ namespace MediaBrowser.Providers.MediaInfo _chapterManager = chapterManager; _libraryManager = libraryManager; _audioResolver = audioResolver; - _mediaSourceManager = mediaSourceManager; + _subtitleResolver = subtitleResolver; } public async Task<ItemUpdateType> ProbeVideo<T>( @@ -215,7 +218,7 @@ namespace MediaBrowser.Providers.MediaInfo chapters = Array.Empty<ChapterInfo>(); } - await AddExternalSubtitles(video, mediaStreams, options, cancellationToken).ConfigureAwait(false); + await AddExternalSubtitlesAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false); await AddExternalAudioAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false); @@ -526,16 +529,14 @@ namespace MediaBrowser.Providers.MediaInfo /// <param name="options">The refreshOptions.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - private async Task AddExternalSubtitles( + private async Task AddExternalSubtitlesAsync( Video video, List<MediaStream> currentStreams, MetadataRefreshOptions options, CancellationToken cancellationToken) { - var subtitleResolver = new SubtitleResolver(_localization); - var startIndex = currentStreams.Count == 0 ? 0 : (currentStreams.Select(i => i.Index).Max() + 1); - var externalSubtitleStreams = subtitleResolver.GetExternalSubtitleStreams(video, startIndex, options.DirectoryService, false); + var externalSubtitleStreams = await _subtitleResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, false, cancellationToken); var enableSubtitleDownloading = options.MetadataRefreshMode == MetadataRefreshMode.Default || options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh; @@ -589,11 +590,11 @@ namespace MediaBrowser.Providers.MediaInfo // Rescan if (downloadedLanguages.Count > 0) { - externalSubtitleStreams = subtitleResolver.GetExternalSubtitleStreams(video, startIndex, options.DirectoryService, true); + externalSubtitleStreams = await _subtitleResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, true, cancellationToken); } } - video.SubtitleFiles = externalSubtitleStreams.Select(i => i.Path).ToArray(); + video.SubtitleFiles = externalSubtitleStreams.Select(i => i.Path).Distinct().ToArray(); currentStreams.AddRange(externalSubtitleStreams); } @@ -612,15 +613,11 @@ namespace MediaBrowser.Providers.MediaInfo CancellationToken cancellationToken) { var startIndex = currentStreams.Count == 0 ? 0 : currentStreams.Max(i => i.Index) + 1; - var externalAudioStreams = _audioResolver.GetExternalAudioStreams(video, startIndex, options.DirectoryService, false, cancellationToken); + var externalAudioStreams = await _audioResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, false, cancellationToken).ConfigureAwait(false); - await foreach (MediaStream externalAudioStream in externalAudioStreams) - { - currentStreams.Add(externalAudioStream); - } + video.AudioFiles = externalAudioStreams.Select(i => i.Path).Distinct().ToArray(); - // Select all external audio file paths - video.AudioFiles = currentStreams.Where(i => i.Type == MediaStreamType.Audio && i.IsExternal).Select(i => i.Path).Distinct().ToArray(); + currentStreams.AddRange(externalAudioStreams); } /// <summary> diff --git a/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs b/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs new file mode 100644 index 000000000..39be405ec --- /dev/null +++ b/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Emby.Naming.Common; +using Emby.Naming.ExternalFiles; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.MediaInfo; + +namespace MediaBrowser.Providers.MediaInfo +{ + /// <summary> + /// Resolves external files for <see cref="Video"/>. + /// </summary> + public abstract class MediaInfoResolver + { + /// <summary> + /// The <see cref="ExternalPathParser"/> instance. + /// </summary> + private readonly ExternalPathParser _externalPathParser; + + /// <summary> + /// The <see cref="IMediaEncoder"/> instance. + /// </summary> + private readonly IMediaEncoder _mediaEncoder; + + private readonly IFileSystem _fileSystem; + + /// <summary> + /// The <see cref="NamingOptions"/> instance. + /// </summary> + private readonly NamingOptions _namingOptions; + + /// <summary> + /// The <see cref="DlnaProfileType"/> of the files this resolver should resolve. + /// </summary> + private readonly DlnaProfileType _type; + + /// <summary> + /// Initializes a new instance of the <see cref="MediaInfoResolver"/> class. + /// </summary> + /// <param name="localizationManager">The localization manager.</param> + /// <param name="mediaEncoder">The media encoder.</param> + /// <param name="fileSystem">The file system.</param> + /// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param> + /// <param name="type">The <see cref="DlnaProfileType"/> of the parsed file.</param> + protected MediaInfoResolver( + ILocalizationManager localizationManager, + IMediaEncoder mediaEncoder, + IFileSystem fileSystem, + NamingOptions namingOptions, + DlnaProfileType type) + { + _mediaEncoder = mediaEncoder; + _fileSystem = fileSystem; + _namingOptions = namingOptions; + _type = type; + _externalPathParser = new ExternalPathParser(namingOptions, localizationManager, _type); + } + + /// <summary> + /// Retrieves the external streams for the provided video. + /// </summary> + /// <param name="video">The <see cref="Video"/> object to search external streams for.</param> + /// <param name="startIndex">The stream index to start adding external streams at.</param> + /// <param name="directoryService">The directory service to search for files.</param> + /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The external streams located.</returns> + public async Task<IReadOnlyList<MediaStream>> GetExternalStreamsAsync( + Video video, + int startIndex, + IDirectoryService directoryService, + bool clearCache, + CancellationToken cancellationToken) + { + if (!video.IsFileProtocol) + { + return Array.Empty<MediaStream>(); + } + + var pathInfos = GetExternalFiles(video, directoryService, clearCache); + + if (!pathInfos.Any()) + { + return Array.Empty<MediaStream>(); + } + + var mediaStreams = new List<MediaStream>(); + + foreach (var pathInfo in pathInfos) + { + var mediaInfo = await GetMediaInfo(pathInfo.Path, _type, cancellationToken).ConfigureAwait(false); + + if (mediaInfo.MediaStreams.Count == 1) + { + MediaStream mediaStream = mediaInfo.MediaStreams[0]; + mediaStream.Index = startIndex++; + mediaStream.IsDefault = pathInfo.IsDefault || mediaStream.IsDefault; + mediaStream.IsForced = pathInfo.IsForced || mediaStream.IsForced; + + mediaStreams.Add(MergeMetadata(mediaStream, pathInfo)); + } + else + { + foreach (MediaStream mediaStream in mediaInfo.MediaStreams) + { + mediaStream.Index = startIndex++; + + mediaStreams.Add(MergeMetadata(mediaStream, pathInfo)); + } + } + } + + return mediaStreams.AsReadOnly(); + } + + /// <summary> + /// Returns the external file infos for the given video. + /// </summary> + /// <param name="video">The <see cref="Video"/> object to search external files for.</param> + /// <param name="directoryService">The directory service to search for files.</param> + /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param> + /// <returns>The external file paths located.</returns> + public IReadOnlyList<ExternalPathParserResult> GetExternalFiles( + Video video, + IDirectoryService directoryService, + bool clearCache) + { + if (!video.IsFileProtocol) + { + return Array.Empty<ExternalPathParserResult>(); + } + + // Check if video folder exists + string folder = video.ContainingFolderPath; + if (!_fileSystem.DirectoryExists(folder)) + { + return Array.Empty<ExternalPathParserResult>(); + } + + var files = directoryService.GetFilePaths(folder, clearCache).ToList(); + var internalMetadataPath = video.GetInternalMetadataPath(); + if (_fileSystem.DirectoryExists(internalMetadataPath)) + { + files.AddRange(directoryService.GetFilePaths(internalMetadataPath, clearCache)); + } + + if (!files.Any()) + { + return Array.Empty<ExternalPathParserResult>(); + } + + var externalPathInfos = new List<ExternalPathParserResult>(); + ReadOnlySpan<char> prefix = video.FileNameWithoutExtension; + foreach (var file in files) + { + var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file.AsSpan()); + if (fileNameWithoutExtension.Length >= prefix.Length + && prefix.Equals(fileNameWithoutExtension[..prefix.Length], StringComparison.OrdinalIgnoreCase) + && (fileNameWithoutExtension.Length == prefix.Length || _namingOptions.MediaFlagDelimiters.Contains(fileNameWithoutExtension[prefix.Length]))) + { + var externalPathInfo = _externalPathParser.ParseFile(file, fileNameWithoutExtension[prefix.Length..].ToString()); + + if (externalPathInfo != null) + { + externalPathInfos.Add(externalPathInfo); + } + } + } + + return externalPathInfos; + } + + /// <summary> + /// Returns the media info of the given file. + /// </summary> + /// <param name="path">The path to the file.</param> + /// <param name="type">The <see cref="DlnaProfileType"/>.</param> + /// <param name="cancellationToken">The cancellation token to cancel operation.</param> + /// <returns>The media info for the given file.</returns> + private Task<Model.MediaInfo.MediaInfo> GetMediaInfo(string path, DlnaProfileType type, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + return _mediaEncoder.GetMediaInfo( + new MediaInfoRequest + { + MediaType = type, + MediaSource = new MediaSourceInfo + { + Path = path, + Protocol = MediaProtocol.File + } + }, + cancellationToken); + } + + /// <summary> + /// Merges path metadata into stream metadata. + /// </summary> + /// <param name="mediaStream">The <see cref="MediaStream"/> object.</param> + /// <param name="pathInfo">The <see cref="ExternalPathParserResult"/> object.</param> + /// <returns>The modified mediaStream.</returns> + private MediaStream MergeMetadata(MediaStream mediaStream, ExternalPathParserResult pathInfo) + { + mediaStream.Path = pathInfo.Path; + mediaStream.IsExternal = true; + mediaStream.Title = string.IsNullOrEmpty(mediaStream.Title) ? (string.IsNullOrEmpty(pathInfo.Title) ? null : pathInfo.Title) : mediaStream.Title; + mediaStream.Language = string.IsNullOrEmpty(mediaStream.Language) ? (string.IsNullOrEmpty(pathInfo.Language) ? null : pathInfo.Language) : mediaStream.Language; + + mediaStream.Type = _type switch + { + DlnaProfileType.Audio => MediaStreamType.Audio, + DlnaProfileType.Subtitle => MediaStreamType.Subtitle, + _ => mediaStream.Type + }; + + return mediaStream; + } + } +} diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs index ba284187e..4b9ba944a 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs @@ -1,235 +1,36 @@ -using System; -using System.Collections.Generic; -using System.IO; +using Emby.Naming.Common; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.IO; namespace MediaBrowser.Providers.MediaInfo { /// <summary> - /// Resolves external subtitles for videos. + /// Resolves external subtitle files for <see cref="Video"/>. /// </summary> - public class SubtitleResolver + public class SubtitleResolver : MediaInfoResolver { - private readonly ILocalizationManager _localization; - - /// <summary> - /// Initializes a new instance of the <see cref="SubtitleResolver"/> class. - /// </summary> - /// <param name="localization">The localization manager.</param> - public SubtitleResolver(ILocalizationManager localization) - { - _localization = localization; - } - - /// <summary> - /// Retrieves the external subtitle streams for the provided video. - /// </summary> - /// <param name="video">The video to search from.</param> - /// <param name="startIndex">The stream index to start adding subtitle streams at.</param> - /// <param name="directoryService">The directory service to search for files.</param> - /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param> - /// <returns>The external subtitle streams located.</returns> - public List<MediaStream> GetExternalSubtitleStreams( - Video video, - int startIndex, - IDirectoryService directoryService, - bool clearCache) - { - var streams = new List<MediaStream>(); - - if (!video.IsFileProtocol) - { - return streams; - } - - AddExternalSubtitleStreams(streams, video.ContainingFolderPath, video.Path, startIndex, directoryService, clearCache); - - startIndex += streams.Count; - - string folder = video.GetInternalMetadataPath(); - - if (!Directory.Exists(folder)) - { - return streams; - } - - try - { - AddExternalSubtitleStreams(streams, folder, video.Path, startIndex, directoryService, clearCache); - } - catch (IOException) - { - } - - return streams; - } - - /// <summary> - /// Locates the external subtitle files for the provided video. - /// </summary> - /// <param name="video">The video to search from.</param> - /// <param name="directoryService">The directory service to search for files.</param> - /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param> - /// <returns>The external subtitle file paths located.</returns> - public IEnumerable<string> GetExternalSubtitleFiles( - Video video, - IDirectoryService directoryService, - bool clearCache) - { - if (!video.IsFileProtocol) - { - yield break; - } - - var streams = GetExternalSubtitleStreams(video, 0, directoryService, clearCache); - - foreach (var stream in streams) - { - yield return stream.Path; - } - } - /// <summary> - /// Extracts the subtitle files from the provided list and adds them to the list of streams. + /// Initializes a new instance of the <see cref="SubtitleResolver"/> class for external subtitle file processing. /// </summary> - /// <param name="streams">The list of streams to add external subtitles to.</param> - /// <param name="videoPath">The path to the video file.</param> - /// <param name="startIndex">The stream index to start adding subtitle streams at.</param> - /// <param name="files">The files to add if they are subtitles.</param> - public void AddExternalSubtitleStreams( - List<MediaStream> streams, - string videoPath, - int startIndex, - IReadOnlyList<string> files) - { - var videoFileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(videoPath); - - for (var i = 0; i < files.Count; i++) - { - var fullName = files[i]; - var extension = Path.GetExtension(fullName.AsSpan()); - if (!IsSubtitleExtension(extension)) - { - continue; - } - - var fileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(fullName); - - MediaStream mediaStream; - - // The subtitle filename must either be equal to the video filename or start with the video filename followed by a dot - if (videoFileNameWithoutExtension.Equals(fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) - { - mediaStream = new MediaStream - { - Index = startIndex++, - Type = MediaStreamType.Subtitle, - IsExternal = true, - Path = fullName - }; - } - else if (fileNameWithoutExtension.Length > videoFileNameWithoutExtension.Length - && fileNameWithoutExtension[videoFileNameWithoutExtension.Length] == '.' - && fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) - { - var isForced = fullName.Contains(".forced.", StringComparison.OrdinalIgnoreCase) - || fullName.Contains(".foreign.", StringComparison.OrdinalIgnoreCase); - - var isDefault = fullName.Contains(".default.", StringComparison.OrdinalIgnoreCase); - - // Support xbmc naming conventions - 300.spanish.srt - var languageSpan = fileNameWithoutExtension; - while (languageSpan.Length > 0) - { - var lastDot = languageSpan.LastIndexOf('.'); - if (lastDot < videoFileNameWithoutExtension.Length) - { - languageSpan = ReadOnlySpan<char>.Empty; - break; - } - - var currentSlice = languageSpan[lastDot..]; - if (currentSlice.Equals(".default", StringComparison.OrdinalIgnoreCase) - || currentSlice.Equals(".forced", StringComparison.OrdinalIgnoreCase) - || currentSlice.Equals(".foreign", StringComparison.OrdinalIgnoreCase)) - { - languageSpan = languageSpan[..lastDot]; - continue; - } - - languageSpan = languageSpan[(lastDot + 1)..]; - break; - } - - var language = languageSpan.ToString(); - if (string.IsNullOrWhiteSpace(language)) - { - language = null; - } - else - { - // Try to translate to three character code - // Be flexible and check against both the full and three character versions - var culture = _localization.FindLanguageInfo(language); - - language = culture == null ? language : culture.ThreeLetterISOLanguageName; - } - - mediaStream = new MediaStream - { - Index = startIndex++, - Type = MediaStreamType.Subtitle, - IsExternal = true, - Path = fullName, - Language = language, - IsForced = isForced, - IsDefault = isDefault - }; - } - else - { - continue; - } - - mediaStream.Codec = extension.TrimStart('.').ToString().ToLowerInvariant(); - - streams.Add(mediaStream); - } - } - - private static bool IsSubtitleExtension(ReadOnlySpan<char> extension) - { - return extension.Equals(".srt", StringComparison.OrdinalIgnoreCase) - || extension.Equals(".ssa", StringComparison.OrdinalIgnoreCase) - || extension.Equals(".ass", StringComparison.OrdinalIgnoreCase) - || extension.Equals(".sub", StringComparison.OrdinalIgnoreCase) - || extension.Equals(".vtt", StringComparison.OrdinalIgnoreCase) - || extension.Equals(".smi", StringComparison.OrdinalIgnoreCase) - || extension.Equals(".sami", StringComparison.OrdinalIgnoreCase); - } - - private static ReadOnlySpan<char> NormalizeFilenameForSubtitleComparison(string filename) - { - // Try to account for sloppy file naming - filename = filename.Replace("_", string.Empty, StringComparison.Ordinal); - filename = filename.Replace(" ", string.Empty, StringComparison.Ordinal); - return Path.GetFileNameWithoutExtension(filename.AsSpan()); - } - - private void AddExternalSubtitleStreams( - List<MediaStream> streams, - string folder, - string videoPath, - int startIndex, - IDirectoryService directoryService, - bool clearCache) + /// <param name="localizationManager">The localization manager.</param> + /// <param name="mediaEncoder">The media encoder.</param> + /// <param name="fileSystem">The file system.</param> + /// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param> + public SubtitleResolver( + ILocalizationManager localizationManager, + IMediaEncoder mediaEncoder, + IFileSystem fileSystem, + NamingOptions namingOptions) + : base( + localizationManager, + mediaEncoder, + fileSystem, + namingOptions, + DlnaProfileType.Subtitle) { - var files = directoryService.GetFilePaths(folder, clearCache, true); - - AddExternalSubtitleStreams(streams, videoPath, startIndex, files); } } } diff --git a/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs b/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs index e0ab31b56..f4941565f 100644 --- a/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs @@ -12,8 +12,7 @@ namespace MediaBrowser.Providers.Plugins.StudioImages { public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages { - // TODO change this for a Jellyfin-hosted repository. - public const string DefaultServer = "https://raw.github.com/MediaBrowser/MediaBrowser.Resources/master/images/imagesbyname"; + public const string DefaultServer = "https://raw.github.com/jellyfin/emby-artwork/master/studios"; public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer) diff --git a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs index 3a3048cec..ce267ac84 100644 --- a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs @@ -50,41 +50,29 @@ namespace MediaBrowser.Providers.Studios { return new List<ImageType> { - ImageType.Primary, ImageType.Thumb }; } - public Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) + public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { - return GetImages(item, true, true, cancellationToken); - } - - private async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, bool posters, bool thumbs, CancellationToken cancellationToken) - { - var list = new List<RemoteImageInfo>(); - - if (posters) - { - var posterPath = Path.Combine(_config.ApplicationPaths.CachePath, "imagesbyname", "remotestudioposters.txt"); + var thumbsPath = Path.Combine(_config.ApplicationPaths.CachePath, "imagesbyname", "remotestudiothumbs.txt"); - posterPath = await EnsurePosterList(posterPath, cancellationToken).ConfigureAwait(false); - - list.Add(GetImage(item, posterPath, ImageType.Primary, "folder")); - } + thumbsPath = await EnsureThumbsList(thumbsPath, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); - if (thumbs) - { - var thumbsPath = Path.Combine(_config.ApplicationPaths.CachePath, "imagesbyname", "remotestudiothumbs.txt"); - - thumbsPath = await EnsureThumbsList(thumbsPath, cancellationToken).ConfigureAwait(false); + var imageInfo = GetImage(item, thumbsPath, ImageType.Thumb, "thumb"); - list.Add(GetImage(item, thumbsPath, ImageType.Thumb, "thumb")); + if (imageInfo == null) + { + return Enumerable.Empty<RemoteImageInfo>(); } - return list.Where(i => i != null); + return new RemoteImageInfo[] + { + imageInfo + }; } private RemoteImageInfo GetImage(BaseItem item, string filename, ImageType type, string remoteFilename) @@ -110,19 +98,12 @@ namespace MediaBrowser.Providers.Studios private string GetUrl(string image, string filename) { - return string.Format(CultureInfo.InvariantCulture, "{0}/{1}/{2}.jpg", repositoryUrl, image, filename); + return string.Format(CultureInfo.InvariantCulture, "{0}/images/{1}/{2}.jpg", repositoryUrl, image, filename); } private Task<string> EnsureThumbsList(string file, CancellationToken cancellationToken) { - string url = string.Format(CultureInfo.InvariantCulture, "{0}/studiothumbs.txt", repositoryUrl); - - return EnsureList(url, file, _fileSystem, cancellationToken); - } - - private Task<string> EnsurePosterList(string file, CancellationToken cancellationToken) - { - string url = string.Format(CultureInfo.InvariantCulture, "{0}/studioposters.txt", repositoryUrl); + string url = string.Format(CultureInfo.InvariantCulture, "{0}/thumbs.txt", repositoryUrl); return EnsureList(url, file, _fileSystem, cancellationToken); } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index f565b6569..4d26052fa 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -264,7 +264,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV series.RunTimeTicks = seriesResult.EpisodeRunTime.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault(); - if (string.Equals(seriesResult.Status, "Ended", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(seriesResult.Status, "Ended", StringComparison.OrdinalIgnoreCase) + || string.Equals(seriesResult.Status, "Canceled", StringComparison.OrdinalIgnoreCase)) { series.Status = SeriesStatus.Ended; series.EndDate = seriesResult.LastAirDate; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs index d78652834..7d7733407 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs @@ -165,8 +165,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb private async Task<TvGroupCollection> GetSeriesGroupAsync(int tvShowId, string displayOrder, string language, string imageLanguages, CancellationToken cancellationToken) { TvGroupType? groupType = + string.Equals(displayOrder, "originalAirDate", StringComparison.Ordinal) ? TvGroupType.OriginalAirDate : string.Equals(displayOrder, "absolute", StringComparison.Ordinal) ? TvGroupType.Absolute : string.Equals(displayOrder, "dvd", StringComparison.Ordinal) ? TvGroupType.DVD : + string.Equals(displayOrder, "digital", StringComparison.Ordinal) ? TvGroupType.Digital : + string.Equals(displayOrder, "storyArc", StringComparison.Ordinal) ? TvGroupType.StoryArc : + string.Equals(displayOrder, "production", StringComparison.Ordinal) ? TvGroupType.Production : + string.Equals(displayOrder, "tv", StringComparison.Ordinal) ? TvGroupType.TV : null; if (groupType == null) diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj index 4d0ba487b..86b05c475 100644 --- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj +++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj @@ -27,7 +27,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index a66b70ac1..6e4f5634d 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -338,7 +338,7 @@ namespace Rssdp.Infrastructure private ISocket ListenForBroadcastsAsync() { - var socket = _SocketFactory.CreateUdpMulticastSocket(SsdpConstants.MulticastLocalAdminAddress, _MulticastTtl, SsdpConstants.MulticastPort); + var socket = _SocketFactory.CreateUdpMulticastSocket(IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress), _MulticastTtl, SsdpConstants.MulticastPort); _ = ListenToSocketInternal(socket); return socket; diff --git a/debian/conf/jellyfin b/debian/conf/jellyfin index ab8d5d1d4..2f0630a9c 100644 --- a/debian/conf/jellyfin +++ b/debian/conf/jellyfin @@ -44,6 +44,8 @@ JELLYFIN_ADDITIONAL_OPTS="" # # SysV init/Upstart options # +# Note: These options are ignored by systemd; use /etc/systemd/system/jellyfin.d overrides instead. +# # Application username JELLYFIN_USER="jellyfin" diff --git a/debian/jellyfin.service b/debian/jellyfin.service index b86f40473..064e10537 100644 --- a/debian/jellyfin.service +++ b/debian/jellyfin.service @@ -6,23 +6,26 @@ After = network-online.target Type = simple EnvironmentFile = /etc/default/jellyfin User = jellyfin +Group = jellyfin +WorkingDirectory = /var/lib/jellyfin ExecStart = /usr/bin/jellyfin ${JELLYFIN_WEB_OPT} ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} ${JELLYFIN_ADDITIONAL_OPTS} Restart = on-failure TimeoutSec = 15 +SuccessExitStatus=0 143 NoNewPrivileges=true SystemCallArchitectures=native RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK -RestrictNamespaces=true +RestrictNamespaces=false RestrictRealtime=true RestrictSUIDSGID=true -ProtectControlGroups=true +ProtectControlGroups=false ProtectHostname=true -ProtectKernelLogs=true -ProtectKernelModules=true -ProtectKernelTunables=true +ProtectKernelLogs=false +ProtectKernelModules=false +ProtectKernelTunables=false LockPersonality=true -PrivateTmp=true +PrivateTmp=false PrivateDevices=false PrivateUsers=true RemoveIPC=true @@ -43,6 +46,5 @@ SystemCallFilter=~@setuid SystemCallFilter=~@swap SystemCallErrorNumber=EPERM - [Install] WantedBy = multi-user.target diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64 index 350f0076a..a4638c03b 100644 --- a/deployment/Dockerfile.centos.amd64 +++ b/deployment/Dockerfile.centos.amd64 @@ -13,7 +13,7 @@ RUN yum update -yq \ && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/e7acb87d-ab08-4620-9050-b3e80f688d36/e93bbadc19b12f81e3a6761719f28b47/dotnet-sdk-6.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c505a449-9ecf-4352-8629-56216f521616/bd6807340faae05b61de340c8bf161e8/dotnet-sdk-6.0.201-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64 index eeff9a96f..8f564bb12 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -12,7 +12,7 @@ RUN dnf update -yq \ && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/e7acb87d-ab08-4620-9050-b3e80f688d36/e93bbadc19b12f81e3a6761719f28b47/dotnet-sdk-6.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c505a449-9ecf-4352-8629-56216f521616/bd6807340faae05b61de340c8bf161e8/dotnet-sdk-6.0.201-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64 index 9d2deb1c6..f5a5b54fc 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -17,7 +17,7 @@ RUN apt-get update -yqq \ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/e7acb87d-ab08-4620-9050-b3e80f688d36/e93bbadc19b12f81e3a6761719f28b47/dotnet-sdk-6.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c505a449-9ecf-4352-8629-56216f521616/bd6807340faae05b61de340c8bf161e8/dotnet-sdk-6.0.201-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64 index ec90dba83..7ca0f6470 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/e7acb87d-ab08-4620-9050-b3e80f688d36/e93bbadc19b12f81e3a6761719f28b47/dotnet-sdk-6.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c505a449-9ecf-4352-8629-56216f521616/bd6807340faae05b61de340c8bf161e8/dotnet-sdk-6.0.201-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf index 3685e16c4..384e49bf0 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/e7acb87d-ab08-4620-9050-b3e80f688d36/e93bbadc19b12f81e3a6761719f28b47/dotnet-sdk-6.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c505a449-9ecf-4352-8629-56216f521616/bd6807340faae05b61de340c8bf161e8/dotnet-sdk-6.0.201-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/fedora/jellyfin.service b/fedora/jellyfin.service index f706b0ad3..1193ddb5b 100644 --- a/fedora/jellyfin.service +++ b/fedora/jellyfin.service @@ -1,15 +1,51 @@ [Unit] -After=network-online.target -Description=Jellyfin is a free software media system that puts you in control of managing and streaming your media. +Description = Jellyfin Media Server +After = network-online.target [Service] -EnvironmentFile=/etc/sysconfig/jellyfin -WorkingDirectory=/var/lib/jellyfin -ExecStart=/usr/bin/jellyfin ${JELLYFIN_WEB_OPT} ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} -TimeoutSec=15 -Restart=on-failure -User=jellyfin -Group=jellyfin +Type = simple +EnvironmentFile = /etc/sysconfig/jellyfin +User = jellyfin +Group = jellyfin +WorkingDirectory = /var/lib/jellyfin +ExecStart = /usr/bin/jellyfin ${JELLYFIN_WEB_OPT} ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} ${JELLYFIN_ADDITIONAL_OPTS} +Restart = on-failure +TimeoutSec = 15 +SuccessExitStatus=0 143 + +NoNewPrivileges=true +SystemCallArchitectures=native +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK +RestrictNamespaces=false +RestrictRealtime=true +RestrictSUIDSGID=true +ProtectClock=true +ProtectControlGroups=false +ProtectHostname=true +ProtectKernelLogs=false +ProtectKernelModules=false +ProtectKernelTunables=false +LockPersonality=true +PrivateTmp=false +PrivateDevices=false +PrivateUsers=true +RemoveIPC=true +SystemCallFilter=~@clock +SystemCallFilter=~@aio +SystemCallFilter=~@chown +SystemCallFilter=~@cpu-emulation +SystemCallFilter=~@debug +SystemCallFilter=~@keyring +SystemCallFilter=~@memlock +SystemCallFilter=~@module +SystemCallFilter=~@mount +SystemCallFilter=~@obsolete +SystemCallFilter=~@privileged +SystemCallFilter=~@raw-io +SystemCallFilter=~@reboot +SystemCallFilter=~@setuid +SystemCallFilter=~@swap +SystemCallErrorNumber=EPERM [Install] -WantedBy=multi-user.target +WantedBy = multi-user.target diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index 37baff5ae..460c43829 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -34,7 +34,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonFlagEnumConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonFlagEnumConverter.cs new file mode 100644 index 000000000..4fa91fa5e --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonFlagEnumConverter.cs @@ -0,0 +1,36 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Extensions.Json.Converters; + +/// <summary> +/// Enum flag to json array converter. +/// </summary> +/// <typeparam name="T">The type of enum.</typeparam> +public class JsonFlagEnumConverter<T> : JsonConverter<T> + where T : struct, Enum +{ + private static readonly T[] _enumValues = Enum.GetValues<T>(); + + /// <inheritdoc /> + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + /// <inheritdoc /> + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + writer.WriteStartArray(); + foreach (var enumValue in _enumValues) + { + if (value.HasFlag(enumValue)) + { + writer.WriteStringValue(enumValue.ToString()); + } + } + + writer.WriteEndArray(); + } +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonFlagEnumConverterFactory.cs b/src/Jellyfin.Extensions/Json/Converters/JsonFlagEnumConverterFactory.cs new file mode 100644 index 000000000..b74caf345 --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonFlagEnumConverterFactory.cs @@ -0,0 +1,24 @@ +using System; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Extensions.Json.Converters; + +/// <summary> +/// Json flag enum converter factory. +/// </summary> +public class JsonFlagEnumConverterFactory : JsonConverterFactory +{ + /// <inheritdoc /> + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsEnum && typeToConvert.IsDefined(typeof(FlagsAttribute)); + } + + /// <inheritdoc /> + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + return (JsonConverter?)Activator.CreateInstance(typeof(JsonFlagEnumConverter<>).MakeGenericType(typeToConvert)); + } +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs index b477bcb66..656e3c3da 100644 --- a/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs +++ b/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs @@ -16,14 +16,15 @@ namespace Jellyfin.Extensions.Json.Converters /// <inheritdoc /> public override void Write(Utf8JsonWriter writer, Guid? value, JsonSerializerOptions options) { - if (value == Guid.Empty) + // null got handled higher up the call stack + var val = value!.Value; + if (val.Equals(default)) { writer.WriteNullValue(); } else { - // null got handled higher up the call stack - JsonGuidConverter.WriteInternal(writer, value!.Value); + JsonGuidConverter.WriteInternal(writer, val); } } } diff --git a/src/Jellyfin.Extensions/Json/JsonDefaults.cs b/src/Jellyfin.Extensions/Json/JsonDefaults.cs index 2cd89dc3b..97cbee971 100644 --- a/src/Jellyfin.Extensions/Json/JsonDefaults.cs +++ b/src/Jellyfin.Extensions/Json/JsonDefaults.cs @@ -36,6 +36,7 @@ namespace Jellyfin.Extensions.Json new JsonGuidConverter(), new JsonNullableGuidConverter(), new JsonVersionConverter(), + new JsonFlagEnumConverterFactory(), new JsonStringEnumConverter(), new JsonNullableStructConverterFactory(), new JsonBoolNumberConverter(), diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj index 56f973a21..30900039d 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj +++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj @@ -12,7 +12,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs b/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs index d80925fc9..a7d52184b 100644 --- a/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs +++ b/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs @@ -39,13 +39,13 @@ public class KeyframeExtractionScheduledTask : IScheduledTask } /// <inheritdoc /> - public string Name => "Keyframe Extractor"; + public string Name => _localizationManager.GetLocalizedString("TaskKeyframeExtractor"); /// <inheritdoc /> public string Key => "KeyframeExtraction"; /// <inheritdoc /> - public string Description => "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time."; + public string Description => _localizationManager.GetLocalizedString("TaskKeyframeExtractorDescription"); /// <inheritdoc /> public string Category => _localizationManager.GetLocalizedString("TasksLibraryCategory"); diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj index 31da11e24..d68c6cca8 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj +++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj @@ -16,12 +16,12 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" /> </ItemGroup> <ItemGroup> diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index 38fb37f17..0b9d8f7c0 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -15,13 +15,13 @@ <PackageReference Include="AutoFixture" Version="4.17.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" /> - <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.2" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.3" /> <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="3.1.2" /> - <PackageReference Include="Moq" Version="4.16.1" /> + <PackageReference Include="Moq" Version="4.17.2" /> </ItemGroup> <!-- Code Analyzers --> @@ -31,7 +31,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj index 7b20823ba..ee51562b4 100644 --- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj +++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj @@ -12,7 +12,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="3.1.2" /> @@ -26,7 +26,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj index 374a17e2f..d2087b023 100644 --- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj +++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj @@ -12,8 +12,8 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> - <PackageReference Include="Moq" Version="4.16.1" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" /> + <PackageReference Include="Moq" Version="4.17.2" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="3.1.2" /> @@ -26,7 +26,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj index d178837b2..c8c526f4d 100644 --- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj +++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj @@ -7,8 +7,8 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> - <PackageReference Include="Moq" Version="4.16.1" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" /> + <PackageReference Include="Moq" Version="4.17.2" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="3.1.2" /> @@ -21,7 +21,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj index 725947577..bd8667a84 100644 --- a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj +++ b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj @@ -7,7 +7,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> @@ -27,7 +27,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonFlagEnumTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonFlagEnumTests.cs new file mode 100644 index 000000000..c8652b323 --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonFlagEnumTests.cs @@ -0,0 +1,28 @@ +using System.Text.Json; +using Jellyfin.Extensions.Json.Converters; +using MediaBrowser.Model.Session; +using Xunit; + +namespace Jellyfin.Extensions.Tests.Json.Converters; + +public class JsonFlagEnumTests +{ + private readonly JsonSerializerOptions _jsonOptions = new() + { + Converters = + { + new JsonFlagEnumConverter<TranscodeReason>() + } + }; + + [Theory] + [InlineData(TranscodeReason.AudioIsExternal | TranscodeReason.ContainerNotSupported, "[\"ContainerNotSupported\",\"AudioIsExternal\"]")] + [InlineData(TranscodeReason.AudioIsExternal | TranscodeReason.ContainerNotSupported | TranscodeReason.VideoBitDepthNotSupported, "[\"ContainerNotSupported\",\"AudioIsExternal\",\"VideoBitDepthNotSupported\"]")] + [InlineData((TranscodeReason)0, "[]")] + public void Serialize_Transcode_Reason(TranscodeReason transcodeReason, string output) + { + var result = JsonSerializer.Serialize(transcodeReason, _jsonOptions); + + Assert.Equal(output, result); + } +} diff --git a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj index 7ab34775a..cc36dc4fa 100644 --- a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj @@ -7,7 +7,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> @@ -26,7 +26,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> <ItemGroup> diff --git a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj index 639c84240..de1fcab59 100644 --- a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj @@ -8,7 +8,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> @@ -27,7 +27,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj index e7534e308..e186a488a 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj @@ -22,8 +22,8 @@ <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" /> <PackageReference Include="coverlet.collector" Version="3.1.2" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> - <PackageReference Include="Moq" Version="4.16.1" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" /> + <PackageReference Include="Moq" Version="4.17.2" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> </ItemGroup> @@ -35,7 +35,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index 0fc8724b6..53e1550ed 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs @@ -31,6 +31,16 @@ namespace Jellyfin.MediaEncoding.Tests.Probing public void GetFrameRate_Success(string value, float? expected) => Assert.Equal(expected, ProbeResultNormalizer.GetFrameRate(value)); + [Theory] + [InlineData(0.5f, "0/1", false)] + [InlineData(24.5f, "8/196", false)] + [InlineData(63.5f, "1/127", true)] + [InlineData(null, "1/60", false)] + [InlineData(30f, "2/120", true)] + [InlineData(59.999996f, "1563/187560", true)] + public void IsCodecTimeBaseDoubleTheFrameRate_Success(float? frameRate, string codecTimeBase, bool expected) + => Assert.Equal(expected, ProbeResultNormalizer.IsCodecTimeBaseDoubleTheFrameRate(frameRate, codecTimeBase)); + [Fact] public void GetMediaInfo_MetaData_Success() { diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs new file mode 100644 index 000000000..4748f3497 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -0,0 +1,466 @@ +using System; +using System.Collections.Specialized; +using System.IO; +using System.Linq; +using System.Runtime.Serialization; +using System.Text.Json; +using System.Threading.Tasks; +using Jellyfin.Extensions.Json; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Session; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; + +namespace Jellyfin.Model.Tests +{ + public class StreamBuilderTests + { + [Theory] + // Chrome + [InlineData("Chrome", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #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, "Transcode")] + [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, 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.DirectPlay)] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #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, "Transcode")] + [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + // Safari + [InlineData("SafariNext", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450 + [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450 + [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450 + // AndroidPixel + [InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("AndroidPixel", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("AndroidPixel", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("AndroidPixel", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("AndroidPixel", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [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.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "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.DirectPlay, (TranscodeReason)0, "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-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-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + // JellyfinMediaPlayer + [InlineData("JellyfinMediaPlayer", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("JellyfinMediaPlayer", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("JellyfinMediaPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("JellyfinMediaPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("JellyfinMediaPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [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.DirectPlay)] // #6450 + [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #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, "Transcode", "http")] + [InlineData("Chrome-NoHLS", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome-NoHLS", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, 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-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", "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")] + [InlineData("TranscodeMedia", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "DirectStream", "http")] + [InlineData("TranscodeMedia", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "http")] + // DirectMedia + [InlineData("DirectMedia", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] + [InlineData("DirectMedia", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] + [InlineData("DirectMedia", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] + [InlineData("DirectMedia", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] + [InlineData("DirectMedia", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] + [InlineData("DirectMedia", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] + [InlineData("DirectMedia", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] + [InlineData("DirectMedia", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] + [InlineData("DirectMedia", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] + // LowBandwidth + [InlineData("LowBandwidth", "mp4-h264-aac-vtt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("LowBandwidth", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("LowBandwidth", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("LowBandwidth", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("LowBandwidth", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("LowBandwidth", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("LowBandwidth", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("LowBandwidth", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + // Null + [InlineData("Null", "mp4-h264-aac-vtt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + [InlineData("Null", "mp4-h264-ac3-aac-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + [InlineData("Null", "mp4-h264-ac3-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + [InlineData("Null", "mp4-hevc-aac-srt-15200k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + [InlineData("Null", "mp4-hevc-ac3-aac-srt-15200k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + [InlineData("Null", "mkv-vp9-aac-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + [InlineData("Null", "mkv-vp9-ac3-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + [InlineData("Null", "mkv-vp9-vorbis-vtt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "") + { + var options = await GetVideoOptions(deviceName, mediaSource); + BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol); + } + + [Theory] + // Chrome + [InlineData("Chrome", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #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.AudioCodecNotSupported)] // #6450 <BUG: this is direct played> + [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #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, "Transcode")] + [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, 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.AudioCodecNotSupported)] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #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, "Transcode")] + [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + // Safari + [InlineData("SafariNext", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450 + [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450 + [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450 + // AndroidPixel + [InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("AndroidPixel", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("AndroidPixel", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("AndroidPixel", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [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.AudioCodecNotSupported)] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #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.AudioCodecNotSupported)] // #6450 + // RokuSSPlus + [InlineData("RokuSSPlus", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay + [InlineData("RokuSSPlus", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [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.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #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 + [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("JellyfinMediaPlayer", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // #6450 + [InlineData("JellyfinMediaPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // #6450 + [InlineData("JellyfinMediaPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("JellyfinMediaPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("JellyfinMediaPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + public async Task BuildVideoItemWithFirstExplicitStream(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "") + { + var options = await GetVideoOptions(deviceName, mediaSource); + options.AudioStreamIndex = 1; + options.SubtitleStreamIndex = options.MediaSources[0].MediaStreams.Count - 1; + + var streamInfo = BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol); + Assert.Equal(streamInfo?.AudioStreamIndex, options.AudioStreamIndex); + Assert.Equal(streamInfo?.SubtitleStreamIndex, options.SubtitleStreamIndex); + } + + [Theory] + // Chrome + [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 + [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + // Firefox + [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + // Yatse + [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + // RokuSSPlus + [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + public async Task BuildVideoItemWithDirectPlayExplicitStreams(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "") + { + var options = await GetVideoOptions(deviceName, mediaSource); + var streamCount = options.MediaSources[0].MediaStreams.Count; + options.AudioStreamIndex = streamCount - 2; + options.SubtitleStreamIndex = streamCount - 1; + + var streamInfo = BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol); + Assert.Equal(streamInfo?.AudioStreamIndex, options.AudioStreamIndex); + Assert.Equal(streamInfo?.SubtitleStreamIndex, options.SubtitleStreamIndex); + } + + private StreamInfo? BuildVideoItemSimpleTest(VideoOptions options, PlayMethod? playMethod, TranscodeReason why, string transcodeMode, string transcodeProtocol) + { + if (string.IsNullOrEmpty(transcodeProtocol)) + { + transcodeProtocol = playMethod == PlayMethod.DirectStream ? "http" : "HLS.ts"; + } + + var builder = GetStreamBuilder(); + + var val = builder.BuildVideoItem(options); + Assert.NotNull(val); + + if (playMethod != null) + { + Assert.Equal(playMethod, val.PlayMethod); + } + + Assert.Equal(why, val.TranscodeReasons); + + var audioStreamIndexInput = options.AudioStreamIndex; + var targetVideoStream = val.TargetVideoStream; + var targetAudioStream = val.TargetAudioStream; + + var mediaSource = options.MediaSources.First(source => source.Id == val.MediaSourceId); + Assert.NotNull(mediaSource); + var videoStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Video); + var audioStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio); + // TODO: check AudioStreamIndex vs options.AudioStreamIndex + var inputAudioStream = mediaSource.GetDefaultAudioStream(audioStreamIndexInput ?? mediaSource.DefaultAudioStreamIndex); + + var uri = ParseUri(val); + + if (playMethod == PlayMethod.DirectPlay) + { + // check expected container + var containers = ContainerProfile.SplitValue(mediaSource.Container); + // TODO: test transcode too + // Assert.Contains(uri.Extension, containers); + + // check expected video codec (1) + Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec); + Assert.Single(val.TargetVideoCodec); + + // check expected audio codecs (1) + Assert.Contains(targetAudioStream.Codec, val.TargetAudioCodec); + Assert.Single(val.TargetAudioCodec); + // Assert.Single(val.AudioCodecs); + + if (transcodeMode == "DirectStream") + { + Assert.Equal(val.Container, uri.Extension); + } + } + else if (playMethod == PlayMethod.DirectStream || playMethod == PlayMethod.Transcode) + { + Assert.NotNull(val.Container); + Assert.NotEmpty(val.VideoCodecs); + Assert.NotEmpty(val.AudioCodecs); + + // check expected container (todo: this could be a test param) + if (transcodeProtocol == "http") + { + // Assert.Equal("webm", val.Container); + Assert.Equal(val.Container, uri.Extension); + Assert.Equal("stream", uri.Filename); + Assert.Equal("http", val.SubProtocol); + } + else if (transcodeProtocol == "HLS.mp4") + { + Assert.Equal("mp4", val.Container); + Assert.Equal("m3u8", uri.Extension); + Assert.Equal("master", uri.Filename); + Assert.Equal("hls", val.SubProtocol); + } + else + { + Assert.Equal("ts", val.Container); + Assert.Equal("m3u8", uri.Extension); + Assert.Equal("master", uri.Filename); + Assert.Equal("hls", val.SubProtocol); + } + + // Full transcode + if (transcodeMode == "Transcode") + { + if ((val.TranscodeReasons & (StreamBuilder.ContainerReasons | TranscodeReason.DirectPlayError)) == 0) + { + Assert.All( + videoStreams, + stream => Assert.DoesNotContain(stream.Codec, val.VideoCodecs)); + } + + // todo: fill out tests here + } + + // DirectStream and Remux + else + { + // check expected video codec (1) + Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec); + Assert.Single(val.TargetVideoCodec); + + if (transcodeMode == "DirectStream") + { + if (!targetAudioStream.IsExternal) + { + // check expected audio codecs (1) + Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs); + } + } + else if (transcodeMode == "Remux") + { + // check expected audio codecs (1) + Assert.Contains(targetAudioStream.Codec, val.AudioCodecs); + Assert.Single(val.AudioCodecs); + } + + // video details + var videoStream = targetVideoStream; + Assert.False(val.EstimateContentLength); + Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo); + Assert.Contains(videoStream.Profile?.ToLowerInvariant() ?? string.Empty, val.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? Array.Empty<string>()); + Assert.Equal(videoStream.Level, val.TargetVideoLevel); + Assert.Equal(videoStream.BitDepth, val.TargetVideoBitDepth); + Assert.InRange(val.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue); + + // audio codec not supported + if ((why & TranscodeReason.AudioCodecNotSupported) != 0) + { + // audio stream specified + if (options.AudioStreamIndex >= 0) + { + // TODO:fixme + if (!targetAudioStream.IsExternal) + { + Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs); + } + } + + // audio stream not specified + else + { + // TODO:fixme + Assert.All(audioStreams, stream => + { + if (!stream.IsExternal) + { + Assert.DoesNotContain(stream.Codec, val.AudioCodecs); + } + }); + } + } + } + } + else if (playMethod == null) + { + Assert.Null(val.SubProtocol); + Assert.Equal("stream", uri.Filename); + + Assert.False(val.EstimateContentLength); + Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo); + } + + return val; + } + + private static async ValueTask<T> TestData<T>(string name) + { + var path = Path.Join("Test Data", typeof(T).Name + "-" + name + ".json"); + using (var stream = File.OpenRead(path)) + { + var value = await JsonSerializer.DeserializeAsync<T>(stream, JsonDefaults.Options); + if (value != null) + { + return value; + } + + throw new SerializationException("Invalid test data: " + name); + } + } + + private StreamBuilder GetStreamBuilder() + { + var transcodeSupport = new Mock<ITranscoderSupport>(); + var logger = new NullLogger<StreamBuilderTests>(); + + return new StreamBuilder(transcodeSupport.Object, logger); + } + + private static async ValueTask<VideoOptions> GetVideoOptions(string deviceProfile, params string[] sources) + { + var mediaSources = sources.Select(src => TestData<MediaSourceInfo>(src)) + .Select(val => val.Result) + .ToArray(); + var mediaSourceId = mediaSources[0]?.Id; + + var dp = await TestData<DeviceProfile>(deviceProfile); + + return new VideoOptions() + { + ItemId = new Guid("11D229B7-2D48-4B95-9F9B-49F6AB75E613"), + MediaSourceId = mediaSourceId, + MediaSources = mediaSources, + DeviceId = "test-deviceId", + Profile = dp, + AllowAudioStreamCopy = true, + AllowVideoStreamCopy = true, + }; + } + + private static (string Path, NameValueCollection Query, string Filename, string Extension) ParseUri(StreamInfo val) + { + var href = val.ToUrl("media:", "ACCESSTOKEN").Split("?", 2); + var path = href[0]; + + var queryString = href.ElementAtOrDefault(1); + var query = string.IsNullOrEmpty(queryString) ? System.Web.HttpUtility.ParseQueryString(queryString ?? string.Empty) : new NameValueCollection(); + + var filename = Path.GetFileNameWithoutExtension(path); + var extension = Path.GetExtension(path); + if (extension.Length > 0) + { + extension = extension.Substring(1); + } + + return (path, query, filename, extension); + } + } +} diff --git a/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs b/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs index 0c97a90b4..9fcf8189f 100644 --- a/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs +++ b/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs @@ -5,23 +5,24 @@ namespace Jellyfin.Model.Tests.Entities { public class MediaStreamTests { - public static TheoryData<MediaStream, string> Get_DisplayTitle_TestData() + public static TheoryData<string, MediaStream> Get_DisplayTitle_TestData() { - var data = new TheoryData<MediaStream, string>(); + var data = new TheoryData<string, MediaStream>(); data.Add( - new MediaStream - { - Type = MediaStreamType.Subtitle, - Title = "English", - Language = string.Empty, - IsForced = false, - IsDefault = false, - Codec = "ASS" - }, - "English - Und - ASS"); + "English - Und - ASS", + new MediaStream + { + Type = MediaStreamType.Subtitle, + Title = "English", + Language = string.Empty, + IsForced = false, + IsDefault = false, + Codec = "ASS" + }); data.Add( + "English - Und", new MediaStream { Type = MediaStreamType.Subtitle, @@ -30,10 +31,10 @@ namespace Jellyfin.Model.Tests.Entities IsForced = false, IsDefault = false, Codec = string.Empty - }, - "English - Und"); + }); data.Add( + "English", new MediaStream { Type = MediaStreamType.Subtitle, @@ -42,10 +43,10 @@ namespace Jellyfin.Model.Tests.Entities IsForced = false, IsDefault = false, Codec = string.Empty - }, - "English"); + }); data.Add( + "English - Default - Forced - SRT", new MediaStream { Type = MediaStreamType.Subtitle, @@ -54,10 +55,23 @@ namespace Jellyfin.Model.Tests.Entities IsForced = true, IsDefault = true, Codec = "SRT" - }, - "English - Default - Forced - SRT"); + }); + + data.Add( + "Title - EN - Default - Forced - SRT - External", + new MediaStream + { + Type = MediaStreamType.Subtitle, + Title = "Title", + Language = "EN", + IsForced = true, + IsDefault = true, + Codec = "SRT", + IsExternal = true + }); data.Add( + "Und", new MediaStream { Type = MediaStreamType.Subtitle, @@ -66,15 +80,27 @@ namespace Jellyfin.Model.Tests.Entities IsForced = false, IsDefault = false, Codec = null - }, - "Und"); + }); + + data.Add( + "Title - AAC - Default - External", + new MediaStream + { + Type = MediaStreamType.Audio, + Title = "Title", + Language = null, + IsForced = false, + IsDefault = true, + Codec = "AAC", + IsExternal = true + }); return data; } [Theory] [MemberData(nameof(Get_DisplayTitle_TestData))] - public void Get_DisplayTitle_should_return_valid_title(MediaStream mediaStream, string expected) + public void Get_DisplayTitle_should_return_valid_title(string expected, MediaStream mediaStream) { Assert.Equal(expected, mediaStream.DisplayTitle); } diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj index 22db5bea2..5fdb22546 100644 --- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj +++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj @@ -7,13 +7,20 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" /> + <PackageReference Include="Moq" Version="4.17.2" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> <PackageReference Include="coverlet.collector" Version="3.1.2" /> <PackageReference Include="FsCheck.Xunit" Version="2.16.4" /> </ItemGroup> + <ItemGroup> + <None Include="Test Data\**\*.*"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + </ItemGroup> + <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> @@ -21,7 +28,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidPixel.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidPixel.json new file mode 100644 index 000000000..68ce3ea4a --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidPixel.json @@ -0,0 +1,332 @@ +{ + "Name": "Jellyfin Android", + "EnableAlbumArtInDidl": false, + "EnableSingleAlbumArtLimit": false, + "EnableSingleSubtitleLimit": false, + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxAlbumArtWidth": 2147483647, + "MaxAlbumArtHeight": 2147483647, + "MaxStreamingBitrate": 8000000, + "MaxStaticBitrate": 8000000, + "MusicStreamingTranscodingBitrate": 128000, + "TimelineOffsetSeconds": 0, + "RequiresPlainVideoItems": false, + "RequiresPlainFolders": false, + "EnableMSMediaReceiverRegistrar": false, + "IgnoreTranscodeByteRangeRequests": false, + "DirectPlayProfiles": [ + { + "Container": "mp4", + "AudioCodec": "mp3,aac,alac,ac3", + "VideoCodec": "h263,mpeg4,h264,hevc,av1", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp4", + "AudioCodec": "mp3,aac,alac,ac3", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "fmp4", + "AudioCodec": "mp3,aac,ac3,eac3", + "VideoCodec": "h263,mpeg4,h264,hevc,av1", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "fmp4", + "AudioCodec": "mp3,aac,ac3,eac3", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webm", + "AudioCodec": "vorbis,opus", + "VideoCodec": "vp8,vp9,av1", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "webm", + "AudioCodec": "vorbis,opus", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "mkv", + "AudioCodec": "pcm_s8,pcm_s16be,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le,pcm_alaw,pcm_mulaw,mp3,aac,vorbis,opus,flac,alac,ac3,eac3,dts,mlp,truehd", + "VideoCodec": "h263,mpeg4,h264,hevc,av1,vp8,vp9,av1", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mkv", + "AudioCodec": "pcm_s8,pcm_s16be,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le,pcm_alaw,pcm_mulaw,mp3,aac,vorbis,opus,flac,alac,ac3,eac3,dts,mlp,truehd", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp3", + "AudioCodec": "mp3", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "ogg", + "AudioCodec": "vorbis,opus,flac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "wav", + "AudioCodec": "pcm_s8,pcm_s16be,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le,pcm_alaw,pcm_mulaw", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "mpegts", + "AudioCodec": "pcm_s8,pcm_s16be,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le,pcm_alaw,pcm_mulaw,mp3,aac,ac3,eac3,dts,mlp,truehd", + "VideoCodec": "mpeg4,h264,hevc", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mpegts", + "AudioCodec": "pcm_s8,pcm_s16be,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le,pcm_alaw,pcm_mulaw,mp3,aac,ac3,eac3,dts,mlp,truehd", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "flv", + "AudioCodec": "mp3,aac", + "VideoCodec": "mpeg4,h264", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "flv", + "AudioCodec": "mp3,aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "aac", + "AudioCodec": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "flac", + "AudioCodec": "flac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "3gp", + "AudioCodec": "3gpp,aac,flac", + "VideoCodec": "h263,mpeg4,h264,hevc", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "3gp", + "AudioCodec": "3gpp,aac,flac", + "Type": "Audio", + "$type": "DirectPlayProfile" + } + ], + "TranscodingProfiles": [ + { + "Container": "ts", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "mp1,mp2,mp3,aac,ac3,eac3,dts,mlp,truehd", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mkv", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "pcm_s8,pcm_s16be,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le,pcm_alaw,pcm_mulaw,mp1,mp2,mp3,aac,vorbis,opus,flac,alac,ac3,eac3,dts,mlp,truehd", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + } + ], + "ContainerProfiles": [ + { + "Type": "Video", + "Container": "mp4", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "mp4", + "$type": "ContainerProfile" + }, + { + "Type": "Video", + "Container": "fmp4", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "fmp4", + "$type": "ContainerProfile" + }, + { + "Type": "Video", + "Container": "webm", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "webm", + "$type": "ContainerProfile" + }, + { + "Type": "Video", + "Container": "mkv", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "mkv", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "mp3", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "ogg", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "wav", + "$type": "ContainerProfile" + }, + { + "Type": "Video", + "Container": "mpegts", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "mpegts", + "$type": "ContainerProfile" + }, + { + "Type": "Video", + "Container": "flv", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "flv", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "aac", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "flac", + "$type": "ContainerProfile" + }, + { + "Type": "Video", + "Container": "3gp", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "3gp", + "$type": "ContainerProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "srt", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "subrip", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ttml", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "srt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "subrip", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ttml", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "vtt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "webvtt", + "Method": "External", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome-NoHLS.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome-NoHLS.json new file mode 100644 index 000000000..5d1f5f162 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome-NoHLS.json @@ -0,0 +1,430 @@ +{ + "EnableAlbumArtInDidl": false, + "EnableSingleAlbumArtLimit": false, + "EnableSingleSubtitleLimit": false, + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxAlbumArtWidth": 0, + "MaxAlbumArtHeight": 0, + "MaxStreamingBitrate": 120000000, + "MaxStaticBitrate": 100000000, + "MusicStreamingTranscodingBitrate": 384000, + "TimelineOffsetSeconds": 0, + "RequiresPlainVideoItems": false, + "RequiresPlainFolders": false, + "EnableMSMediaReceiverRegistrar": false, + "IgnoreTranscodeByteRangeRequests": false, + "DirectPlayProfiles": [ + { + "Container": "webm", + "AudioCodec": "vorbis,opus", + "VideoCodec": "vp8,vp9,av1", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp4,m4v", + "AudioCodec": "aac,mp3,opus,flac,alac,vorbis", + "VideoCodec": "h264,vp8,vp9,av1", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mov", + "AudioCodec": "aac,mp3,opus,flac,alac,vorbis", + "VideoCodec": "h264", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "opus", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webm", + "AudioCodec": "opus", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4a", + "AudioCodec": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4b", + "AudioCodec": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "flac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4a", + "AudioCodec": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4b", + "AudioCodec": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webma", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webm", + "AudioCodec": "webma", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "ogg", + "Type": "Audio", + "$type": "DirectPlayProfile" + } + ], + "TranscodingProfiles": [ + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "opus", + "Type": "Audio", + "AudioCodec": "opus", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "AudioCodec": "wav", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "opus", + "Type": "Audio", + "AudioCodec": "opus", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "AudioCodec": "wav", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp4", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "aac,mp3,opus,flac,alac,vorbis", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "webm", + "Type": "Video", + "VideoCodec": "vp8,vp9,av1,vpx", + "AudioCodec": "vorbis,opus", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp4", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "aac,mp3,opus,flac,alac,vorbis", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + } + ], + "CodecProfiles": [ + { + "Type": "VideoAudio", + "Conditions": [ + { + "Condition": "Equals", + "Property": "IsSecondaryAudio", + "Value": "false", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "aac", + "$type": "CodecProfile" + }, + { + "Type": "VideoAudio", + "Conditions": [ + { + "Condition": "Equals", + "Property": "IsSecondaryAudio", + "Value": "false", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "$type": "CodecProfile" + }, + { + "Type": "Video", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "high|main|baseline|constrained baseline|high 10", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "52", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "NotEquals", + "Property": "IsInterlaced", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "h264", + "$type": "CodecProfile" + }, + { + "Type": "Video", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "main", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "120", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "NotEquals", + "Property": "IsInterlaced", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "hevc", + "$type": "CodecProfile" + } + ], + "ResponseProfiles": [ + { + "Container": "m4v", + "Type": "Video", + "MimeType": "video/mp4", + "$type": "ResponseProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "vtt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "External", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome.json new file mode 100644 index 000000000..81bb97ac8 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome.json @@ -0,0 +1,448 @@ +{ + "EnableAlbumArtInDidl": false, + "EnableSingleAlbumArtLimit": false, + "EnableSingleSubtitleLimit": false, + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxAlbumArtWidth": 0, + "MaxAlbumArtHeight": 0, + "MaxStreamingBitrate": 120000000, + "MaxStaticBitrate": 100000000, + "MusicStreamingTranscodingBitrate": 384000, + "TimelineOffsetSeconds": 0, + "RequiresPlainVideoItems": false, + "RequiresPlainFolders": false, + "EnableMSMediaReceiverRegistrar": false, + "IgnoreTranscodeByteRangeRequests": false, + "DirectPlayProfiles": [ + { + "Container": "webm", + "AudioCodec": "vorbis,opus", + "VideoCodec": "vp8,vp9,av1", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp4,m4v", + "AudioCodec": "aac,mp3,opus,flac,alac,vorbis", + "VideoCodec": "h264,vp8,vp9,av1", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mov", + "AudioCodec": "aac,mp3,opus,flac,alac,vorbis", + "VideoCodec": "h264", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "opus", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webm", + "AudioCodec": "opus", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4a", + "AudioCodec": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4b", + "AudioCodec": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "flac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4a", + "AudioCodec": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4b", + "AudioCodec": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webma", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webm", + "AudioCodec": "webma", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "ogg", + "Type": "Audio", + "$type": "DirectPlayProfile" + } + ], + "TranscodingProfiles": [ + { + "Container": "ts", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 2, + "SegmentLength": 0, + "BreakOnNonKeyFrames": true, + "$type": "TranscodingProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "opus", + "Type": "Audio", + "AudioCodec": "opus", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "AudioCodec": "wav", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "opus", + "Type": "Audio", + "AudioCodec": "opus", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "AudioCodec": "wav", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "ts", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "aac,mp3", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 2, + "SegmentLength": 0, + "BreakOnNonKeyFrames": true, + "$type": "TranscodingProfile" + }, + { + "Container": "webm", + "Type": "Video", + "VideoCodec": "vp8,vp9,av1,vpx", + "AudioCodec": "vorbis,opus", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp4", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "aac,mp3,opus,flac,alac,vorbis", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + } + ], + "CodecProfiles": [ + { + "Type": "VideoAudio", + "Conditions": [ + { + "Condition": "Equals", + "Property": "IsSecondaryAudio", + "Value": "false", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "aac", + "$type": "CodecProfile" + }, + { + "Type": "VideoAudio", + "Conditions": [ + { + "Condition": "Equals", + "Property": "IsSecondaryAudio", + "Value": "false", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "$type": "CodecProfile" + }, + { + "Type": "Video", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "high|main|baseline|constrained baseline|high 10", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "52", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "NotEquals", + "Property": "IsInterlaced", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "h264", + "$type": "CodecProfile" + }, + { + "Type": "Video", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "main", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "120", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "NotEquals", + "Property": "IsInterlaced", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "hevc", + "$type": "CodecProfile" + } + ], + "ResponseProfiles": [ + { + "Container": "m4v", + "Type": "Video", + "MimeType": "video/mp4", + "$type": "ResponseProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "vtt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "External", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-DirectMedia.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-DirectMedia.json new file mode 100644 index 000000000..d1df7341e --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-DirectMedia.json @@ -0,0 +1,90 @@ +{ + "Name": "Jellyfin Media Player", + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxStreamingBitrate": 20000000, + "MaxStaticBitrate": 20000000, + "MusicStreamingTranscodingBitrate": 1280000, + "TimelineOffsetSeconds": 5, + "DirectPlayProfiles": [ + { + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Type": "Photo", + "$type": "DirectPlayProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "srt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "srt", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "smi", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "smi", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "pgssub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "dvdsub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "pgs", + "Method": "Embed", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Firefox.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Firefox.json new file mode 100644 index 000000000..9874793d3 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Firefox.json @@ -0,0 +1,441 @@ +{ + "EnableAlbumArtInDidl": false, + "EnableSingleAlbumArtLimit": false, + "EnableSingleSubtitleLimit": false, + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxAlbumArtWidth": 0, + "MaxAlbumArtHeight": 0, + "MaxStreamingBitrate": 120000000, + "MaxStaticBitrate": 100000000, + "MusicStreamingTranscodingBitrate": 384000, + "TimelineOffsetSeconds": 0, + "RequiresPlainVideoItems": false, + "RequiresPlainFolders": false, + "EnableMSMediaReceiverRegistrar": false, + "IgnoreTranscodeByteRangeRequests": false, + "DirectPlayProfiles": [ + { + "Container": "webm", + "AudioCodec": "vorbis,opus", + "VideoCodec": "vp8,vp9,av1", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp4,m4v", + "AudioCodec": "aac,mp3,opus,flac,alac,vorbis", + "VideoCodec": "h264,vp8,vp9,av1", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "opus", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webm", + "AudioCodec": "opus", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4a", + "AudioCodec": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4b", + "AudioCodec": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "flac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4a", + "AudioCodec": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4b", + "AudioCodec": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webma", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webm", + "AudioCodec": "webma", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "ogg", + "Type": "Audio", + "$type": "DirectPlayProfile" + } + ], + "TranscodingProfiles": [ + { + "Container": "ts", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 2, + "SegmentLength": 0, + "BreakOnNonKeyFrames": true, + "$type": "TranscodingProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "opus", + "Type": "Audio", + "AudioCodec": "opus", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "AudioCodec": "wav", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "opus", + "Type": "Audio", + "AudioCodec": "opus", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "AudioCodec": "wav", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "ts", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "aac,mp3", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 2, + "SegmentLength": 0, + "BreakOnNonKeyFrames": true, + "$type": "TranscodingProfile" + }, + { + "Container": "webm", + "Type": "Video", + "VideoCodec": "vp8,vp9,av1,vpx", + "AudioCodec": "vorbis,opus", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp4", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "aac,mp3,opus,flac,alac,vorbis", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + } + ], + "CodecProfiles": [ + { + "Type": "VideoAudio", + "Conditions": [ + { + "Condition": "Equals", + "Property": "IsSecondaryAudio", + "Value": "false", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "aac", + "$type": "CodecProfile" + }, + { + "Type": "VideoAudio", + "Conditions": [ + { + "Condition": "Equals", + "Property": "IsSecondaryAudio", + "Value": "false", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "$type": "CodecProfile" + }, + { + "Type": "Video", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "high|main|baseline|constrained baseline", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "52", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "NotEquals", + "Property": "IsInterlaced", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "h264", + "$type": "CodecProfile" + }, + { + "Type": "Video", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "main", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "120", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "NotEquals", + "Property": "IsInterlaced", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "hevc", + "$type": "CodecProfile" + } + ], + "ResponseProfiles": [ + { + "Container": "m4v", + "Type": "Video", + "MimeType": "video/mp4", + "$type": "ResponseProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "vtt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "External", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-JellyfinMediaPlayer.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-JellyfinMediaPlayer.json new file mode 100644 index 000000000..da9a1a4ad --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-JellyfinMediaPlayer.json @@ -0,0 +1,137 @@ +{ + "Name": "Jellyfin Media Player", + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxStreamingBitrate": 8000000, + "MaxStaticBitrate": 8000000, + "MusicStreamingTranscodingBitrate": 1280000, + "TimelineOffsetSeconds": 5, + "DirectPlayProfiles": [ + { + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Type": "Photo", + "$type": "DirectPlayProfile" + } + ], + "TranscodingProfiles": [ + { + "Type": "Audio", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "ts", + "Type": "Video", + "VideoCodec": "h264,h265,hevc,mpeg4,mpeg2video", + "AudioCodec": "aac,mp3,ac3,opus,flac,vorbis", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "jpeg", + "Type": "Photo", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "srt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "srt", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "smi", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "smi", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "pgssub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "dvdsub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "pgs", + "Method": "Embed", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-LowBandwidth.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-LowBandwidth.json new file mode 100644 index 000000000..82b73fb0f --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-LowBandwidth.json @@ -0,0 +1,137 @@ +{ + "Name": "Jellyfin Media Player", + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxStreamingBitrate": 120000, + "MaxStaticBitrate": 100000, + "MusicStreamingTranscodingBitrate": 3840, + "TimelineOffsetSeconds": 5, + "DirectPlayProfiles": [ + { + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Type": "Photo", + "$type": "DirectPlayProfile" + } + ], + "TranscodingProfiles": [ + { + "Type": "Audio", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "ts", + "Type": "Video", + "VideoCodec": "h264,h265,hevc,mpeg4,mpeg2video", + "AudioCodec": "aac,mp3,ac3,opus,flac,vorbis", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "jpeg", + "Type": "Photo", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "srt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "srt", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "smi", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "smi", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "pgssub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "dvdsub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "pgs", + "Method": "Embed", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Null.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Null.json new file mode 100644 index 000000000..d463bd896 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Null.json @@ -0,0 +1,9 @@ +{ + "Name": "Jellyfin Media Player", + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxStreamingBitrate": 120000, + "MaxStaticBitrate": 100000, + "MusicStreamingTranscodingBitrate": 3840, + "TimelineOffsetSeconds": 5, + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlus.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlus.json new file mode 100644 index 000000000..37b923558 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlus.json @@ -0,0 +1,211 @@ +{ + "EnableAlbumArtInDidl": false, + "EnableSingleAlbumArtLimit": false, + "EnableSingleSubtitleLimit": false, + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxAlbumArtWidth": 0, + "MaxAlbumArtHeight": 0, + "MaxStreamingBitrate": 120000000, + "MaxStaticBitrate": 100000000, + "MusicStreamingTranscodingBitrate": 192000, + "TimelineOffsetSeconds": 0, + "RequiresPlainVideoItems": false, + "RequiresPlainFolders": false, + "EnableMSMediaReceiverRegistrar": false, + "IgnoreTranscodeByteRangeRequests": false, + "DirectPlayProfiles": [ + { + "Container": "mp4,m4v,mov", + "AudioCodec": "mp3,pcm,lpcm,wav,alac,aac", + "VideoCodec": "h264,h265,hevc,mpeg2video", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mkv,webm", + "AudioCodec": "mp3,pcm,lpcm,wav,flac,alac,aac,opus,vorbis", + "VideoCodec": "h264,vp8,h265,hevc,vp9,mpeg2video", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp3,pcm,lpcm,wav,wma,flac,alac,aac,wmapro", + "Type": "Audio", + "$type": "DirectPlayProfile" + } + ], + "TranscodingProfiles": [ + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": " 2", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "2", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "2", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": " 2", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "ts", + "Type": "Video", + "VideoCodec": "h264,mpeg2video", + "AudioCodec": "aac", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": " 2", + "MinSegments": 1, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp4", + "Type": "Video", + "VideoCodec": "h264,h265,hevc,mpeg2video", + "AudioCodec": "mp3,pcm,lpcm,wav,alac,aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + } + ], + "CodecProfiles": [ + { + "Type": "Video", + "Conditions": [ + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "high|main|baseline|constrained baseline", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "51", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "h264", + "$type": "CodecProfile" + }, + { + "Type": "Video", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "main|main 10", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "NotEquals", + "Property": "IsInterlaced", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "hevc", + "$type": "CodecProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "vtt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "srt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ttml", + "Method": "External", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlusNext.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlusNext.json new file mode 100644 index 000000000..542bf6370 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlusNext.json @@ -0,0 +1,211 @@ +{ + "EnableAlbumArtInDidl": false, + "EnableSingleAlbumArtLimit": false, + "EnableSingleSubtitleLimit": false, + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxAlbumArtWidth": 0, + "MaxAlbumArtHeight": 0, + "MaxStreamingBitrate": 120000000, + "MaxStaticBitrate": 100000000, + "MusicStreamingTranscodingBitrate": 192000, + "TimelineOffsetSeconds": 0, + "RequiresPlainVideoItems": false, + "RequiresPlainFolders": false, + "EnableMSMediaReceiverRegistrar": false, + "IgnoreTranscodeByteRangeRequests": false, + "DirectPlayProfiles": [ + { + "Container": "mp4,m4v,mov", + "AudioCodec": "mp3,pcm,lpcm,wav,alac,aac", + "VideoCodec": "h264,h265,hevc,mpeg2video", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mkv,webm", + "AudioCodec": "mp3,pcm,lpcm,wav,flac,alac,aac,opus,vorbis", + "VideoCodec": "h264,vp8,h265,hevc,vp9,mpeg2video", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp3,pcm,lpcm,wav,wma,flac,alac,aac,wmapro", + "Type": "Audio", + "$type": "DirectPlayProfile" + } + ], + "TranscodingProfiles": [ + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": " 2", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "2", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "2", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": " 2", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "ts", + "Type": "Video", + "VideoCodec": "h264,h265,hevc,mpeg2video", + "AudioCodec": "aac", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": " 2", + "MinSegments": 1, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp4", + "Type": "Video", + "VideoCodec": "h264,h265,hevc,mpeg2video", + "AudioCodec": "mp3,pcm,lpcm,wav,alac,aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + } + ], + "CodecProfiles": [ + { + "Type": "Video", + "Conditions": [ + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "high|main|baseline|constrained baseline", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "51", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "h264", + "$type": "CodecProfile" + }, + { + "Type": "Video", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "main|main 10", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "NotEquals", + "Property": "IsInterlaced", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "hevc", + "$type": "CodecProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "vtt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "srt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ttml", + "Method": "External", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-SafariNext.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-SafariNext.json new file mode 100644 index 000000000..3b5a0c254 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-SafariNext.json @@ -0,0 +1,357 @@ +{ + "EnableAlbumArtInDidl": false, + "EnableSingleAlbumArtLimit": false, + "EnableSingleSubtitleLimit": false, + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxAlbumArtWidth": 0, + "MaxAlbumArtHeight": 0, + "MaxStreamingBitrate": 120000000, + "MaxStaticBitrate": 100000000, + "MusicStreamingTranscodingBitrate": 384000, + "TimelineOffsetSeconds": 0, + "RequiresPlainVideoItems": false, + "RequiresPlainFolders": false, + "EnableMSMediaReceiverRegistrar": false, + "IgnoreTranscodeByteRangeRequests": false, + "DirectPlayProfiles": [ + { + "Container": "webm", + "AudioCodec": "vorbis", + "VideoCodec": "vp8,vp9", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp4,m4v", + "AudioCodec": "aac,mp3,ac3,eac3,flac,alac,vorbis", + "VideoCodec": "h264,vp8,vp9", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mov", + "AudioCodec": "aac,mp3,ac3,eac3,flac,alac,vorbis", + "VideoCodec": "h264", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4a", + "AudioCodec": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4b", + "AudioCodec": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "flac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4a", + "AudioCodec": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4b", + "AudioCodec": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webma", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webm", + "AudioCodec": "webma", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "$type": "DirectPlayProfile" + } + ], + "TranscodingProfiles": [ + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 2, + "SegmentLength": 0, + "BreakOnNonKeyFrames": true, + "$type": "TranscodingProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "AudioCodec": "wav", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "AudioCodec": "wav", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp4", + "Type": "Video", + "AudioCodec": "aac,ac3,eac3,flac,alac", + "VideoCodec": "hevc,h264", + "Context": "Streaming", + "Protocol": "hls", + "MaxAudioChannels": "2", + "MinSegments": "2", + "BreakOnNonKeyFrames": true + }, + { + "Container": "ts", + "Type": "Video", + "AudioCodec": "aac,mp3,ac3,eac3", + "VideoCodec": "h264", + "Context": "Streaming", + "Protocol": "hls", + "MaxAudioChannels": "2", + "MinSegments": "2", + "BreakOnNonKeyFrames": true + }, + { + "Container": "webm", + "Type": "Video", + "AudioCodec": "vorbis", + "VideoCodec": "vp8,vpx", + "Context": "Streaming", + "Protocol": "http", + "MaxAudioChannels": "2" + }, + { + "Container": "mp4", + "Type": "Video", + "AudioCodec": "aac,mp3,ac3,eac3,flac,alac,vorbis", + "VideoCodec": "h264", + "Context": "Static", + "Protocol": "http" + } + ], + "CodecProfiles": [ + { + "Type": "Video", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "high|main|baseline|constrained baseline", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "52", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "NotEquals", + "Property": "IsInterlaced", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "h264", + "$type": "CodecProfile" + }, + { + "Type": "Video", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "main|main 10", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "183", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "NotEquals", + "Property": "IsInterlaced", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "hevc", + "$type": "CodecProfile" + } + ], + "ResponseProfiles": [ + { + "Container": "m4v", + "Type": "Video", + "MimeType": "video/mp4", + "$type": "ResponseProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "vtt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "External", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-TranscodeMedia.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-TranscodeMedia.json new file mode 100644 index 000000000..9fc1ae6bb --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-TranscodeMedia.json @@ -0,0 +1,139 @@ +{ + "Name": "Jellyfin Media Player", + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxStreamingBitrate": 20000000, + "MaxStaticBitrate": 20000000, + "MusicStreamingTranscodingBitrate": 1280000, + "TimelineOffsetSeconds": 5, + "TranscodingProfiles": [ + { + "Type": "Audio", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp4", + "Type": "Video", + "AudioCodec": "aac,flac,alac", + "VideoCodec": "hevc,h264", + "Context": "Streaming", + "Protocol": "hls", + "MaxAudioChannels": "2", + "MinSegments": "2", + "BreakOnNonKeyFrames": true, + "$type": "TranscodingProfile" + }, + { + "Container": "ts", + "Type": "Video", + "AudioCodec": "aac,mp3", + "VideoCodec": "h264", + "Context": "Streaming", + "Protocol": "hls", + "MaxAudioChannels": "2", + "MinSegments": "2", + "BreakOnNonKeyFrames": true, + "$type": "TranscodingProfile" + }, + { + "Container": "webm", + "Type": "Video", + "AudioCodec": "vorbis", + "VideoCodec": "vp9,vp8,vpx,av1", + "Context": "Streaming", + "Protocol": "http", + "MaxAudioChannels": "2", + "$type": "TranscodingProfile" + }, + { + "Container": "jpeg", + "Type": "Photo", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "srt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "srt", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "smi", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "smi", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "pgssub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "dvdsub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "pgs", + "Method": "Embed", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse.json new file mode 100644 index 000000000..256c8dc2f --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse.json @@ -0,0 +1,189 @@ +{ + "EnableAlbumArtInDidl": false, + "EnableSingleAlbumArtLimit": false, + "EnableSingleSubtitleLimit": false, + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxAlbumArtWidth": 0, + "MaxAlbumArtHeight": 0, + "MaxStreamingBitrate": 120000000, + "MaxStaticBitrate": 100000000, + "MusicStreamingTranscodingBitrate": 192000, + "TimelineOffsetSeconds": 0, + "RequiresPlainVideoItems": false, + "RequiresPlainFolders": false, + "EnableMSMediaReceiverRegistrar": false, + "IgnoreTranscodeByteRangeRequests": false, + "DirectPlayProfiles": [ + { + "Container": "", + "AudioCodec": "aac", + "VideoCodec": "", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "ts,mp4,mka,m4a,mp3,mp2,wav,flac,ogg", + "AudioCodec": "", + "VideoCodec": "", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "", + "AudioCodec": "", + "VideoCodec": "", + "Type": "Photo", + "$type": "DirectPlayProfile" + } + ], + "TranscodingProfiles": [ + { + "Container": "ts", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "aac", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": true, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "VideoCodec": "", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "VideoCodec": "", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + } + ], + "CodecProfiles": [ + { + "Type": "VideoAudio", + "Conditions": [ + { + "Condition": "Equals", + "Property": "IsSecondaryAudio", + "Value": "false", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "", + "Container": "", + "$type": "CodecProfile" + } + ], + "ResponseProfiles": [ + { + "Container": "m4v", + "Type": "Video", + "MimeType": "video/mp4", + "$type": "ResponseProfile" + }, + { + "Container": "mov", + "Type": "Video", + "MimeType": "video/webm", + "$type": "ResponseProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "vtt", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "srt", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "smi", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "subrip", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "dvdsub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "pgs", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "pgssub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "srt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "External", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse2.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse2.json new file mode 100644 index 000000000..256c8dc2f --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse2.json @@ -0,0 +1,189 @@ +{ + "EnableAlbumArtInDidl": false, + "EnableSingleAlbumArtLimit": false, + "EnableSingleSubtitleLimit": false, + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxAlbumArtWidth": 0, + "MaxAlbumArtHeight": 0, + "MaxStreamingBitrate": 120000000, + "MaxStaticBitrate": 100000000, + "MusicStreamingTranscodingBitrate": 192000, + "TimelineOffsetSeconds": 0, + "RequiresPlainVideoItems": false, + "RequiresPlainFolders": false, + "EnableMSMediaReceiverRegistrar": false, + "IgnoreTranscodeByteRangeRequests": false, + "DirectPlayProfiles": [ + { + "Container": "", + "AudioCodec": "aac", + "VideoCodec": "", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "ts,mp4,mka,m4a,mp3,mp2,wav,flac,ogg", + "AudioCodec": "", + "VideoCodec": "", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "", + "AudioCodec": "", + "VideoCodec": "", + "Type": "Photo", + "$type": "DirectPlayProfile" + } + ], + "TranscodingProfiles": [ + { + "Container": "ts", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "aac", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": true, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "VideoCodec": "", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "VideoCodec": "", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + } + ], + "CodecProfiles": [ + { + "Type": "VideoAudio", + "Conditions": [ + { + "Condition": "Equals", + "Property": "IsSecondaryAudio", + "Value": "false", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "", + "Container": "", + "$type": "CodecProfile" + } + ], + "ResponseProfiles": [ + { + "Container": "m4v", + "Type": "Video", + "MimeType": "video/mp4", + "$type": "ResponseProfile" + }, + { + "Container": "mov", + "Type": "Video", + "MimeType": "video/webm", + "$type": "ResponseProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "vtt", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "srt", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "smi", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "subrip", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "dvdsub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "pgs", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "pgssub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "srt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "External", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-av1-aac-srt-2600k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-av1-aac-srt-2600k.json new file mode 100644 index 000000000..da185aacf --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-av1-aac-srt-2600k.json @@ -0,0 +1,73 @@ +{ + "Id": "a766d122b58e45d9492d17af66748bf5", + "Path": "/Media/MyVideo-720p.mkv", + "Container": "mkv,webm", + "Size": 835317696, + "Name": "MyVideo-1080p", + "ETag": "579a34c6d5dfb23f61539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "av1", + "Language": "eng", + "ColorTransfer": "bt709", + "ColorPrimaries": "bt709", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "1080p AV1 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "Main", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": -99 + }, + { + "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": 1, + "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": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-av1-vorbis-srt-2600k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-av1-vorbis-srt-2600k.json new file mode 100644 index 000000000..774dba32a --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-av1-vorbis-srt-2600k.json @@ -0,0 +1,72 @@ +{ + "Id": "a766d122b58e45d9492d17af66748bf5", + "Path": "/Media/MyVideo-720p.mkv", + "Container": "mkv,webm", + "Size": 835317696, + "Name": "MyVideo-1080p", + "ETag": "579a34c6d5dfb23f61539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "av1", + "Language": "eng", + "ColorTransfer": "bt709", + "ColorPrimaries": "bt709", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "1080p AV1 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "Main", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": -99 + }, + { + "Codec": "vorbis", + "CodecTag": "ogg", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - Vorbis - Stereo - Default", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "Index": 1, + "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": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-vp9-aac-srt-2600k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-vp9-aac-srt-2600k.json new file mode 100644 index 000000000..0a85a1353 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-vp9-aac-srt-2600k.json @@ -0,0 +1,73 @@ +{ + "Id": "a766d122b58e45d9492d17af66748bf5", + "Path": "/Media/MyVideo-720p.mkv", + "Container": "mkv,webm", + "Size": 835317696, + "Name": "MyVideo-1080p", + "ETag": "579a34c6d5dfb23f61539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "vp9", + "Language": "eng", + "ColorTransfer": "bt709", + "ColorPrimaries": "bt709", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "1080p VP9 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "Profile 0", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": -99 + }, + { + "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": 1, + "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": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-vp9-ac3-srt-2600k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-vp9-ac3-srt-2600k.json new file mode 100644 index 000000000..2b932ff52 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-vp9-ac3-srt-2600k.json @@ -0,0 +1,72 @@ +{ + "Id": "a766d122b58e45d9492d17af66748bf5", + "Path": "/Media/MyVideo-720p.mkv", + "Container": "mkv,webm", + "Size": 835317696, + "Name": "MyVideo-1080p", + "ETag": "579a34c6d5dfb23f61539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "vp9", + "Language": "eng", + "ColorTransfer": "bt709", + "ColorPrimaries": "bt709", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "1080p VP9 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "Profile 0", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": -99 + }, + { + "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": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-vp9-vorbis-srt-2600k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-vp9-vorbis-srt-2600k.json new file mode 100644 index 000000000..56b04b789 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-vp9-vorbis-srt-2600k.json @@ -0,0 +1,73 @@ +{ + "Id": "a766d122b58e45d9492d17af66748bf5", + "Path": "/Media/MyVideo-720p.mkv", + "Container": "mkv,webm", + "Size": 835317696, + "Name": "MyVideo-1080p", + "ETag": "579a34c6d5dfb23f61539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "vp9", + "Language": "eng", + "ColorTransfer": "bt709", + "ColorPrimaries": "bt709", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "1080p VP9 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "Profile 0", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": -99 + }, + { + "Codec": "vorbis", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - Vorbis - Stereo - Default", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "Profile": "LC", + "Index": 1, + "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": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + } + ], + "Bitrate": 2590008, + "RequiredHttpHeaders": {}, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-vp9-vorbis-vtt-2600k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-vp9-vorbis-vtt-2600k.json new file mode 100644 index 000000000..1ee7eade9 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-vp9-vorbis-vtt-2600k.json @@ -0,0 +1,72 @@ +{ + "Id": "a766d122b58e45d9492d17af66748bf5", + "Path": "/Media/MyVideo-720p.mkv", + "Container": "mkv,webm", + "Size": 835317696, + "Name": "MyVideo-1080p", + "ETag": "579a34c6d5dfb23f61539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "vp9", + "Language": "eng", + "ColorTransfer": "bt709", + "ColorPrimaries": "bt709", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "1080p VP9 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "Profile 0", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": -99 + }, + { + "Codec": "vorbis", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - Vorbis - Stereo - Default", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "Profile": "LC", + "Index": 1, + "Score": 203 + }, + { + "Codec": "webvtt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-aac-srt-2600k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-aac-srt-2600k.json new file mode 100644 index 000000000..21911843d --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-aac-srt-2600k.json @@ -0,0 +1,72 @@ +{ + "Id": "a766d122b58e45d9492d17af77748bf5", + "Path": "/Media/MyVideo-720p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 835317696, + "Name": "MyVideo-720p", + "ETag": "579a34c6d5dfb21d81539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "h264", + "CodecTag": "avc1", + "Language": "eng", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "720p H264 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "High", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": 41 + }, + { + "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": 1, + "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": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-aac-vtt-2600k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-aac-vtt-2600k.json new file mode 100644 index 000000000..77954a31a --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-aac-vtt-2600k.json @@ -0,0 +1,72 @@ +{ + "Id": "a766d122b58e45d9492d17af77748bf5", + "Path": "/Media/MyVideo-720p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 835317696, + "Name": "MyVideo-720p", + "ETag": "579a34c6d5dfb21d81539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "h264", + "CodecTag": "avc1", + "Language": "eng", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "720p H264 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "High", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": 41 + }, + { + "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": 1, + "Score": 203 + }, + { + "Codec": "webvtt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} 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 new file mode 100644 index 000000000..70bbb9d0d --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-srt-2600k.json @@ -0,0 +1,87 @@ +{ + "Id": "a766d122b58e45d9492d17af77748bf5", + "Path": "/Media/MyVideo-720p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 835317696, + "Name": "MyVideo-720p", + "ETag": "579a34c6d5dfb21d81539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "h264", + "CodecTag": "avc1", + "Language": "eng", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "720p H264 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "High", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": 41 + }, + { + "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": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 3 +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aacDef-srt-2600k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aacDef-srt-2600k.json new file mode 100644 index 000000000..036e41f07 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aacDef-srt-2600k.json @@ -0,0 +1,87 @@ +{ + "Id": "a766d122b58e45d9492d17af77748bf5", + "Path": "/Media/MyVideo-720p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 835317696, + "Name": "MyVideo-720p", + "ETag": "579a34c6d5dfb21d81539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "h264", + "CodecTag": "avc1", + "Language": "eng", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "720p H264 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "High", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": 41 + }, + { + "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": 2590008, + "DefaultAudioStreamIndex": 2, + "DefaultSubtitleStreamIndex": 3 +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aacExt-srt-2600k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aacExt-srt-2600k.json new file mode 100644 index 000000000..b81c4597f --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aacExt-srt-2600k.json @@ -0,0 +1,89 @@ +{ + "Id": "a766d122b58e45d9492d17af77748bf5", + "Path": "/Media/MyVideo-720p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 835317696, + "Name": "MyVideo-720p", + "ETag": "579a34c6d5dfb21d81539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "h264", + "CodecTag": "avc1", + "Language": "eng", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "720p H264 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "High", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": 41 + }, + { + "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, + "IsExternal": true, + "Profile": "LC", + "Index": 2, + "Score": 203, + "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt" + }, + { + "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": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 3 +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-srt-2600k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-srt-2600k.json new file mode 100644 index 000000000..b71fd4a6a --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-srt-2600k.json @@ -0,0 +1,71 @@ +{ + "Id": "a766d122b58e45d9492d17af77748bf5", + "Path": "/Media/MyVideo-720p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 835317696, + "Name": "MyVideo-720p", + "ETag": "579a34c6d5dfb21d81539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "h264", + "CodecTag": "avc1", + "Language": "eng", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "720p H264 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "High", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": 41 + }, + { + "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": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-aac-srt-15200k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-aac-srt-15200k.json new file mode 100644 index 000000000..4c6409e7b --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-aac-srt-15200k.json @@ -0,0 +1,75 @@ +{ + "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": "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": 1, + "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": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt" + } + ], + "Bitrate": 15201382, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} 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 new file mode 100644 index 000000000..385bb7260 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aac-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": 1, + "DefaultSubtitleStreamIndex": 3 +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aacExt-srt-15200k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aacExt-srt-15200k.json new file mode 100644 index 000000000..fd1950bde --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aacExt-srt-15200k.json @@ -0,0 +1,91 @@ +{ + "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, + "IsExternal": true, + "Profile": "LC", + "Index": 2, + "Score": 203, + "Path": "/Media/MyVideo-WEBDL-2160p.eng.m4a" + }, + { + "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": 1, + "DefaultSubtitleStreamIndex": 3 +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-srt-15200k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-srt-15200k.json new file mode 100644 index 000000000..dde7c15ea --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-srt-15200k.json @@ -0,0 +1,74 @@ +{ + "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": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt" + } + ], + "Bitrate": 15201382, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-raw.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-raw.json new file mode 100644 index 000000000..9ea55b805 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-raw.json @@ -0,0 +1,102 @@ +{ + "Id": "a766d122b58e45d9492d17af77748bf5", + "Path": "/Media/MyVideo-720p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 835317696, + "Name": "MyVideo-720p", + "ETag": "579a34c6d5dfb21d81539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "h264", + "CodecTag": "avc1", + "Language": "eng", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "720p H264 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "High", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": 41 + }, + { + "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": "mov_text", + "CodecTag": "tx3g", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 3, + "Score": 6421, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "IsDefault": true, + "Type": 2, + "Index": 4, + "Score": 6422, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt" + } + ], + "Bitrate": 2590008, + "RequiredHttpHeaders": {}, + "DefaultSubtitleStreamIndex": 1 +} diff --git a/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs b/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs new file mode 100644 index 000000000..b396b5440 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs @@ -0,0 +1,111 @@ +using System.Text.RegularExpressions; +using Emby.Naming.Common; +using Emby.Naming.ExternalFiles; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Globalization; +using Moq; +using Xunit; + +namespace Jellyfin.Naming.Tests.ExternalFiles; + +public class ExternalPathParserTests +{ + private readonly ExternalPathParser _audioPathParser; + private readonly ExternalPathParser _subtitlePathParser; + + public ExternalPathParserTests() + { + var englishCultureDto = new CultureDto("English", "English", "en", new[] { "eng" }); + var frenchCultureDto = new CultureDto("French", "French", "fr", new[] { "fre", "fra" }); + + var localizationManager = new Mock<ILocalizationManager>(MockBehavior.Loose); + localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"en.*", RegexOptions.IgnoreCase))) + .Returns(englishCultureDto); + localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"fr.*", RegexOptions.IgnoreCase))) + .Returns(frenchCultureDto); + + _audioPathParser = new ExternalPathParser(new NamingOptions(), localizationManager.Object, DlnaProfileType.Audio); + _subtitlePathParser = new ExternalPathParser(new NamingOptions(), localizationManager.Object, DlnaProfileType.Subtitle); + } + + [Theory] + [InlineData("")] + [InlineData("MyVideo.ass")] + [InlineData("MyVideo.mks")] + [InlineData("MyVideo.sami")] + [InlineData("MyVideo.srt")] + [InlineData("MyVideo.m4v")] + public void ParseFile_AudioExtensionsNotMatched_ReturnsNull(string path) + { + Assert.Null(_audioPathParser.ParseFile(path, string.Empty)); + } + + [Theory] + [InlineData("MyVideo.aa")] + [InlineData("MyVideo.aac")] + [InlineData("MyVideo.flac")] + [InlineData("MyVideo.m4a")] + [InlineData("MyVideo.mka")] + [InlineData("MyVideo.mp3")] + public void ParseFile_AudioExtensionsMatched_ReturnsPath(string path) + { + var actual = _audioPathParser.ParseFile(path, string.Empty); + Assert.NotNull(actual); + Assert.Equal(path, actual!.Path); + } + + [Theory] + [InlineData("")] + [InlineData("MyVideo.aa")] + [InlineData("MyVideo.aac")] + [InlineData("MyVideo.flac")] + [InlineData("MyVideo.mka")] + [InlineData("MyVideo.m4v")] + public void ParseFile_SubtitleExtensionsNotMatched_ReturnsNull(string path) + { + Assert.Null(_subtitlePathParser.ParseFile(path, string.Empty)); + } + + [Theory] + [InlineData("MyVideo.ass")] + [InlineData("MyVideo.mks")] + [InlineData("MyVideo.sami")] + [InlineData("MyVideo.srt")] + [InlineData("MyVideo.vtt")] + public void ParseFile_SubtitleExtensionsMatched_ReturnsPath(string path) + { + var actual = _subtitlePathParser.ParseFile(path, string.Empty); + Assert.NotNull(actual); + Assert.Equal(path, actual!.Path); + } + + [Theory] + [InlineData("", null, null)] + [InlineData(".default", null, null, true, false)] + [InlineData(".forced", null, null, false, true)] + [InlineData(".foreign", null, null, false, true)] + [InlineData(".default.forced", null, null, true, true)] + [InlineData(".forced.default", null, null, true, true)] + [InlineData(".DEFAULT.FORCED", null, null, true, true)] + [InlineData(".en", null, "eng")] + [InlineData(".EN", null, "eng")] + [InlineData(".fr.en", "fr", "eng")] + [InlineData(".en.fr", "en", "fre")] + [InlineData(".title.en.fr", "title.en", "fre")] + [InlineData(".Title Goes Here", "Title Goes Here", null)] + [InlineData(".Title.with.Separator", "Title.with.Separator", null)] + [InlineData(".title.en.default.forced", "title", "eng", true, true)] + [InlineData(".forced.default.en.title", "title", "eng", true, true)] + public void ParseFile_ExtraTokens_ParseToValues(string tokens, string? title, string? language, bool isDefault = false, bool isForced = false) + { + var path = "My.Video" + tokens + ".srt"; + + var actual = _subtitlePathParser.ParseFile(path, tokens); + + Assert.NotNull(actual); + Assert.Equal(title, actual!.Title); + Assert.Equal(language, actual.Language); + Assert.Equal(isDefault, actual.IsDefault); + Assert.Equal(isForced, actual.IsForced); + } +} diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj index 59b7e1cbe..69ba5d86a 100644 --- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj +++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj @@ -12,7 +12,8 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" /> + <PackageReference Include="Moq" Version="4.17.2" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="3.1.2" /> @@ -29,7 +30,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs deleted file mode 100644 index 2446660f3..000000000 --- a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Emby.Naming.Common; -using Emby.Naming.Subtitles; -using Xunit; - -namespace Jellyfin.Naming.Tests.Subtitles -{ - public class SubtitleParserTests - { - private readonly NamingOptions _namingOptions = new NamingOptions(); - - [Theory] - [InlineData("The Skin I Live In (2011).srt", null, false, false)] - [InlineData("The Skin I Live In (2011).eng.srt", "eng", false, false)] - [InlineData("The Skin I Live In (2011).eng.default.srt", "eng", true, false)] - [InlineData("The Skin I Live In (2011).eng.forced.srt", "eng", false, true)] - [InlineData("The Skin I Live In (2011).eng.foreign.srt", "eng", false, true)] - [InlineData("The Skin I Live In (2011).eng.default.foreign.srt", "eng", true, true)] - [InlineData("The Skin I Live In (2011).default.foreign.eng.srt", "eng", true, true)] - public void SubtitleParser_ValidFileName_Parses(string input, string language, bool isDefault, bool isForced) - { - var parser = new SubtitleParser(_namingOptions); - - var result = parser.ParseFile(input); - - Assert.Equal(language, result?.Language, true); - Assert.Equal(isDefault, result?.IsDefault); - Assert.Equal(isForced, result?.IsForced); - Assert.Equal(input, result?.Path); - } - - [Theory] - [InlineData("The Skin I Live In (2011).mp4")] - [InlineData("")] - public void SubtitleParser_InvalidFileName_ReturnsNull(string input) - { - var parser = new SubtitleParser(_namingOptions); - - Assert.Null(parser.ParseFile(input)); - } - } -} diff --git a/tests/Jellyfin.Networking.Tests/Configuration/NetworkConfigurationTests.cs b/tests/Jellyfin.Networking.Tests/Configuration/NetworkConfigurationTests.cs new file mode 100644 index 000000000..a78b872df --- /dev/null +++ b/tests/Jellyfin.Networking.Tests/Configuration/NetworkConfigurationTests.cs @@ -0,0 +1,28 @@ +using Jellyfin.Networking.Configuration; +using Xunit; + +namespace Jellyfin.Networking.Tests.Configuration; + +public static class NetworkConfigurationTests +{ + [Theory] + [InlineData("", null)] + [InlineData("", "")] + [InlineData("/Test", "/Test")] + [InlineData("/Test", "Test")] + [InlineData("/Test", "Test/")] + [InlineData("/Test", "/Test/")] + [InlineData("/Test/2", "/Test/2")] + [InlineData("/Test/2", "Test/2")] + [InlineData("/Test/2", "Test/2/")] + [InlineData("/Test/2", "/Test/2/")] + public static void BaseUrl_ReturnsNormalized(string expected, string input) + { + var config = new NetworkConfiguration() + { + BaseUrl = input + }; + + Assert.Equal(expected, config.BaseUrl); + } +} diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj index 3d3288df6..ba39cc0ff 100644 --- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj +++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj @@ -12,12 +12,12 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> <PackageReference Include="coverlet.collector" Version="3.1.2" /> <PackageReference Include="FsCheck.Xunit" Version="2.16.4" /> - <PackageReference Include="Moq" Version="4.16.1" /> + <PackageReference Include="Moq" Version="4.17.2" /> </ItemGroup> <!-- Code Analyzers--> @@ -27,7 +27,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj index 9f571273f..6d52a3cd6 100644 --- a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj +++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj @@ -13,8 +13,8 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> - <PackageReference Include="Moq" Version="4.16.1" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" /> + <PackageReference Include="Moq" Version="4.17.2" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> @@ -33,7 +33,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/AudioResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/AudioResolverTests.cs new file mode 100644 index 000000000..aec523882 --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/AudioResolverTests.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Emby.Naming.Common; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.IO; +using MediaBrowser.Providers.MediaInfo; +using Moq; +using Xunit; + +namespace Jellyfin.Providers.Tests.MediaInfo; + +public class AudioResolverTests +{ + private readonly AudioResolver _audioResolver; + + public AudioResolverTests() + { + // prep BaseItem and Video for calls made that expect managers + Video.LiveTvManager = Mock.Of<ILiveTvManager>(); + + var applicationPaths = new Mock<IServerApplicationPaths>().Object; + var serverConfig = new Mock<IServerConfigurationManager>(); + serverConfig.Setup(c => c.ApplicationPaths) + .Returns(applicationPaths); + BaseItem.ConfigurationManager = serverConfig.Object; + + // build resolver to test with + var localizationManager = Mock.Of<ILocalizationManager>(); + + var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict); + mediaEncoder.Setup(me => me.GetMediaInfo(It.IsAny<MediaInfoRequest>(), It.IsAny<CancellationToken>())) + .Returns<MediaInfoRequest, CancellationToken>((_, _) => Task.FromResult(new MediaBrowser.Model.MediaInfo.MediaInfo + { + MediaStreams = new List<MediaStream> + { + new() + } + })); + + var fileSystem = new Mock<IFileSystem>(MockBehavior.Strict); + fileSystem.Setup(fs => fs.DirectoryExists(It.IsRegex(MediaInfoResolverTests.VideoDirectoryRegex))) + .Returns(true); + fileSystem.Setup(fs => fs.DirectoryExists(It.IsRegex(MediaInfoResolverTests.MetadataDirectoryRegex))) + .Returns(true); + + _audioResolver = new AudioResolver(localizationManager, mediaEncoder.Object, fileSystem.Object, new NamingOptions()); + } + + [Theory] + [InlineData("My.Video.srt", false, false)] + [InlineData("My.Video.mp3", false, true)] + [InlineData("My.Video.srt", true, false)] + [InlineData("My.Video.mp3", true, true)] + public async void GetExternalStreams_MixedFilenames_PicksAudio(string file, bool metadataDirectory, bool matches) + { + BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>(); + + var video = new Movie + { + Path = MediaInfoResolverTests.VideoDirectoryPath + "/My.Video.mkv" + }; + + var directoryService = MediaInfoResolverTests.GetDirectoryServiceForExternalFile(file, metadataDirectory); + var streams = await _audioResolver.GetExternalStreamsAsync(video, 0, directoryService, false, CancellationToken.None); + + if (matches) + { + Assert.Single(streams); + var actual = streams[0]; + Assert.Equal(MediaStreamType.Audio, actual.Type); + } + else + { + Assert.Empty(streams); + } + } +} diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs index 98ac1dd64..9b80f0b94 100644 --- a/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs @@ -148,7 +148,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo var mediaSourceManager = new Mock<IMediaSourceManager>(MockBehavior.Strict); mediaSourceManager.Setup(i => i.GetMediaAttachments(item.Id)) .Returns(mediaAttachments); - mediaSourceManager.Setup(i => i.GetMediaStreams(It.Is<MediaStreamQuery>(q => q.ItemId == item.Id && q.Type == MediaStreamType.EmbeddedImage))) + mediaSourceManager.Setup(i => i.GetMediaStreams(It.Is<MediaStreamQuery>(q => q.ItemId.Equals(item.Id) && q.Type == MediaStreamType.EmbeddedImage))) .Returns(mediaStreams); return mediaSourceManager.Object; } diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs new file mode 100644 index 000000000..98b4a6ccf --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs @@ -0,0 +1,435 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Emby.Naming.Common; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Providers.MediaInfo; +using Moq; +using Xunit; + +namespace Jellyfin.Providers.Tests.MediaInfo; + +public class MediaInfoResolverTests +{ + public const string VideoDirectoryPath = "Test Data/Video"; + public const string VideoDirectoryRegex = @"Test Data[/\\]Video"; + public const string MetadataDirectoryPath = "library/00/00000000000000000000000000000000"; + public const string MetadataDirectoryRegex = @"library.*"; + + private readonly ILocalizationManager _localizationManager; + private readonly MediaInfoResolver _subtitleResolver; + + public MediaInfoResolverTests() + { + // prep BaseItem and Video for calls made that expect managers + Video.LiveTvManager = Mock.Of<ILiveTvManager>(); + + var applicationPaths = new Mock<IServerApplicationPaths>().Object; + var serverConfig = new Mock<IServerConfigurationManager>(); + serverConfig.Setup(c => c.ApplicationPaths) + .Returns(applicationPaths); + BaseItem.ConfigurationManager = serverConfig.Object; + + // build resolver to test with + var englishCultureDto = new CultureDto("English", "English", "en", new[] { "eng" }); + + var localizationManager = new Mock<ILocalizationManager>(MockBehavior.Loose); + localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"en.*", RegexOptions.IgnoreCase))) + .Returns(englishCultureDto); + _localizationManager = localizationManager.Object; + + var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict); + mediaEncoder.Setup(me => me.GetMediaInfo(It.IsAny<MediaInfoRequest>(), It.IsAny<CancellationToken>())) + .Returns<MediaInfoRequest, CancellationToken>((_, _) => Task.FromResult(new MediaBrowser.Model.MediaInfo.MediaInfo + { + MediaStreams = new List<MediaStream> + { + new() + } + })); + + var fileSystem = new Mock<IFileSystem>(MockBehavior.Strict); + fileSystem.Setup(fs => fs.DirectoryExists(It.IsAny<string>())) + .Returns(false); + fileSystem.Setup(fs => fs.DirectoryExists(It.IsRegex(VideoDirectoryRegex))) + .Returns(true); + fileSystem.Setup(fs => fs.DirectoryExists(It.IsRegex(MetadataDirectoryRegex))) + .Returns(true); + + _subtitleResolver = new SubtitleResolver(_localizationManager, mediaEncoder.Object, fileSystem.Object, new NamingOptions()); + } + + [Fact] + public void GetExternalFiles_BadProtocol_ReturnsNoSubtitles() + { + // need a media source manager capable of returning something other than file protocol + var mediaSourceManager = new Mock<IMediaSourceManager>(); + mediaSourceManager.Setup(m => m.GetPathProtocol(It.IsRegex(@"http.*"))) + .Returns(MediaProtocol.Http); + BaseItem.MediaSourceManager = mediaSourceManager.Object; + + var video = new Movie + { + Path = "https://url.com/My.Video.mkv" + }; + + Assert.Empty(_subtitleResolver.GetExternalFiles(video, Mock.Of<IDirectoryService>(), false)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void GetExternalFiles_MissingDirectory_DirectoryNotQueried(bool metadataDirectory) + { + BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>(); + + string containingFolderPath, metadataPath; + + if (metadataDirectory) + { + containingFolderPath = VideoDirectoryPath; + metadataPath = "invalid"; + } + else + { + containingFolderPath = "invalid"; + metadataPath = MetadataDirectoryPath; + } + + var video = new Mock<Movie>(); + video.Setup(m => m.Path) + .Returns(VideoDirectoryPath + "/My.Video.mkv"); + video.Setup(m => m.ContainingFolderPath) + .Returns(containingFolderPath); + video.Setup(m => m.GetInternalMetadataPath()) + .Returns(metadataPath); + + string pathNotFoundRegex = metadataDirectory ? MetadataDirectoryRegex : VideoDirectoryRegex; + + var directoryService = new Mock<IDirectoryService>(MockBehavior.Strict); + // any path other than test target exists and provides an empty listing + directoryService.Setup(ds => ds.GetFilePaths(It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<bool>())) + .Returns(Array.Empty<string>()); + + _subtitleResolver.GetExternalFiles(video.Object, directoryService.Object, false); + + directoryService.Verify( + ds => ds.GetFilePaths(It.IsRegex(pathNotFoundRegex), It.IsAny<bool>(), It.IsAny<bool>()), + Times.Never); + } + + [Theory] + [InlineData("My.Video.mkv", "My.Video.srt", null)] + [InlineData("My.Video.mkv", "My.Video.en.srt", "eng")] + [InlineData("My.Video.mkv", "My.Video.en.srt", "eng", true)] + [InlineData("Example Movie (2021).mp4", "Example Movie (2021).English.Srt", "eng")] + [InlineData("[LTDB] Who Framed Roger Rabbit (1998) - [Bluray-1080p].mkv", "[LTDB] Who Framed Roger Rabbit (1998) - [Bluray-1080p].en.srt", "eng")] + public void GetExternalFiles_NameMatching_MatchesAndParsesToken(string movie, string file, string? language, bool metadataDirectory = false) + { + BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>(); + + var video = new Movie + { + Path = VideoDirectoryPath + "/" + movie + }; + + var directoryService = GetDirectoryServiceForExternalFile(file, metadataDirectory); + var streams = _subtitleResolver.GetExternalFiles(video, directoryService, false).ToList(); + + Assert.Single(streams); + var actual = streams[0]; + Assert.Equal(language, actual.Language); + Assert.Null(actual.Title); + } + + [Theory] + [InlineData("cover.jpg")] + [InlineData("My.Video.mp3")] + [InlineData("My.Video.png")] + [InlineData("My.Video.txt")] + [InlineData("My.Video Sequel.srt")] + [InlineData("Some.Other.Video.srt")] + public void GetExternalFiles_NameMatching_RejectsNonMatches(string file) + { + BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>(); + + var video = new Movie + { + Path = VideoDirectoryPath + "/My.Video.mkv" + }; + + var directoryService = GetDirectoryServiceForExternalFile(file); + var streams = _subtitleResolver.GetExternalFiles(video, directoryService, false).ToList(); + + Assert.Empty(streams); + } + + [Theory] + [InlineData("https://url.com/My.Video.mkv")] + [InlineData(VideoDirectoryPath)] // valid but no files found for this test + public async void GetExternalStreams_BadPaths_ReturnsNoSubtitles(string path) + { + // need a media source manager capable of returning something other than file protocol + var mediaSourceManager = new Mock<IMediaSourceManager>(); + mediaSourceManager.Setup(m => m.GetPathProtocol(It.IsRegex(@"http.*"))) + .Returns(MediaProtocol.Http); + BaseItem.MediaSourceManager = mediaSourceManager.Object; + + var video = new Movie + { + Path = path + }; + + var directoryService = new Mock<IDirectoryService>(MockBehavior.Strict); + directoryService.Setup(ds => ds.GetFilePaths(It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<bool>())) + .Returns(Array.Empty<string>()); + + var mediaEncoder = Mock.Of<IMediaEncoder>(MockBehavior.Strict); + var fileSystem = Mock.Of<IFileSystem>(); + + var subtitleResolver = new SubtitleResolver(_localizationManager, mediaEncoder, fileSystem, new NamingOptions()); + + var streams = await subtitleResolver.GetExternalStreamsAsync(video, 0, directoryService.Object, false, CancellationToken.None); + + Assert.Empty(streams); + } + + private static TheoryData<string, MediaStream[], MediaStream[]> GetExternalStreams_MergeMetadata_HandlesOverridesCorrectly_Data() + { + var data = new TheoryData<string, MediaStream[], MediaStream[]>(); + + // filename and stream have no metadata set + string file = "My.Video.srt"; + data.Add( + file, + new[] + { + CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0) + }, + new[] + { + CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0) + }); + + // filename has metadata + file = "My.Video.Title1.default.forced.en.srt"; + data.Add( + file, + new[] + { + CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0) + }, + new[] + { + CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title1", 0, true, true) + }); + + // single stream with metadata + file = "My.Video.mks"; + data.Add( + file, + new[] + { + CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, true) + }, + new[] + { + CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, true) + }); + + // stream wins for title/language, filename wins for flags when conflicting + file = "My.Video.Title2.default.forced.en.srt"; + data.Add( + file, + new[] + { + CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 0) + }, + new[] + { + CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 0, true, true) + }); + + // multiple stream with metadata - filename flags ignored but other data filled in when missing from stream + file = "My.Video.Title3.default.forced.en.srt"; + data.Add( + file, + new[] + { + CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0, true, true), + CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 1) + }, + new[] + { + CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title3", 0, true, true), + CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 1) + }); + + return data; + } + + [Theory] + [MemberData(nameof(GetExternalStreams_MergeMetadata_HandlesOverridesCorrectly_Data))] + public async void GetExternalStreams_MergeMetadata_HandlesOverridesCorrectly(string file, MediaStream[] inputStreams, MediaStream[] expectedStreams) + { + BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>(); + + var video = new Movie + { + Path = VideoDirectoryPath + "/My.Video.mkv" + }; + + var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict); + mediaEncoder.Setup(me => me.GetMediaInfo(It.IsAny<MediaInfoRequest>(), It.IsAny<CancellationToken>())) + .Returns<MediaInfoRequest, CancellationToken>((_, _) => Task.FromResult(new MediaBrowser.Model.MediaInfo.MediaInfo + { + MediaStreams = inputStreams.ToList() + })); + + var fileSystem = new Mock<IFileSystem>(MockBehavior.Strict); + fileSystem.Setup(fs => fs.DirectoryExists(It.IsRegex(VideoDirectoryRegex))) + .Returns(true); + fileSystem.Setup(fs => fs.DirectoryExists(It.IsRegex(MetadataDirectoryRegex))) + .Returns(true); + + var subtitleResolver = new SubtitleResolver(_localizationManager, mediaEncoder.Object, fileSystem.Object, new NamingOptions()); + + var directoryService = GetDirectoryServiceForExternalFile(file); + var streams = await subtitleResolver.GetExternalStreamsAsync(video, 0, directoryService, false, CancellationToken.None); + + Assert.Equal(expectedStreams.Length, streams.Count); + for (var i = 0; i < expectedStreams.Length; i++) + { + var expected = expectedStreams[i]; + var actual = streams[i]; + + Assert.True(actual.IsExternal); + Assert.Equal(expected.Index, actual.Index); + Assert.Equal(expected.Type, actual.Type); + Assert.Equal(expected.Path, actual.Path); + Assert.Equal(expected.IsDefault, actual.IsDefault); + Assert.Equal(expected.IsForced, actual.IsForced); + Assert.Equal(expected.Language, actual.Language); + Assert.Equal(expected.Title, actual.Title); + } + } + + [Theory] + [InlineData(1, 1)] + [InlineData(1, 2)] + [InlineData(2, 1)] + [InlineData(2, 2)] + public async void GetExternalStreams_StreamIndex_HandlesFilesAndContainers(int fileCount, int streamCount) + { + BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>(); + + var video = new Movie + { + Path = VideoDirectoryPath + "/My.Video.mkv" + }; + + var files = new string[fileCount]; + for (int i = 0; i < fileCount; i++) + { + files[i] = $"{VideoDirectoryPath}/My.Video.{i}.srt"; + } + + var directoryService = new Mock<IDirectoryService>(MockBehavior.Strict); + directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(VideoDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>())) + .Returns(files); + directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(MetadataDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>())) + .Returns(Array.Empty<string>()); + + List<MediaStream> GenerateMediaStreams() + { + var mediaStreams = new List<MediaStream>(); + for (int i = 0; i < streamCount; i++) + { + mediaStreams.Add(new()); + } + + return mediaStreams; + } + + var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict); + mediaEncoder.Setup(me => me.GetMediaInfo(It.IsAny<MediaInfoRequest>(), It.IsAny<CancellationToken>())) + .Returns<MediaInfoRequest, CancellationToken>((_, _) => Task.FromResult(new MediaBrowser.Model.MediaInfo.MediaInfo + { + MediaStreams = GenerateMediaStreams() + })); + + var fileSystem = new Mock<IFileSystem>(MockBehavior.Strict); + fileSystem.Setup(fs => fs.DirectoryExists(It.IsRegex(VideoDirectoryRegex))) + .Returns(true); + fileSystem.Setup(fs => fs.DirectoryExists(It.IsRegex(MetadataDirectoryRegex))) + .Returns(true); + + var subtitleResolver = new SubtitleResolver(_localizationManager, mediaEncoder.Object, fileSystem.Object, new NamingOptions()); + + int startIndex = 1; + var streams = await subtitleResolver.GetExternalStreamsAsync(video, startIndex, directoryService.Object, false, CancellationToken.None); + + Assert.Equal(fileCount * streamCount, streams.Count); + for (var i = 0; i < streams.Count; i++) + { + Assert.Equal(startIndex + i, streams[i].Index); + // intentional integer division to ensure correct number of streams come back from each file + Assert.Matches(@$".*\.{i / streamCount}\.srt", streams[i].Path); + } + } + + private static MediaStream CreateMediaStream(string path, string? language, string? title, int index, bool isForced = false, bool isDefault = false) + { + return new MediaStream + { + Index = index, + Type = MediaStreamType.Subtitle, + Path = path, + IsDefault = isDefault, + IsForced = isForced, + Language = language, + Title = title + }; + } + + /// <summary> + /// Provides an <see cref="IDirectoryService"/> that when queried for the test video/metadata directory will return a path including the provided file name. + /// </summary> + /// <param name="file">The name of the file to locate.</param> + /// <param name="useMetadataDirectory"><c>true</c> if the file belongs in the metadata directory.</param> + /// <returns>A mocked <see cref="IDirectoryService"/>.</returns> + public static IDirectoryService GetDirectoryServiceForExternalFile(string file, bool useMetadataDirectory = false) + { + var directoryService = new Mock<IDirectoryService>(MockBehavior.Strict); + if (useMetadataDirectory) + { + directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(VideoDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>())) + .Returns(Array.Empty<string>()); + directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(MetadataDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>())) + .Returns(new[] { MetadataDirectoryPath + "/" + file }); + } + else + { + directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(VideoDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>())) + .Returns(new[] { VideoDirectoryPath + "/" + file }); + directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(MetadataDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>())) + .Returns(Array.Empty<string>()); + } + + return directoryService.Object; + } +} diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs index 040ea5d1d..0e6457ce3 100644 --- a/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs @@ -1,129 +1,86 @@ -#pragma warning disable CA1002 // Do not expose generic lists - using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Emby.Naming.Common; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.IO; using MediaBrowser.Providers.MediaInfo; using Moq; using Xunit; -namespace Jellyfin.Providers.Tests.MediaInfo +namespace Jellyfin.Providers.Tests.MediaInfo; + +public class SubtitleResolverTests { - public class SubtitleResolverTests - { - public static TheoryData<List<MediaStream>, string, int, string[], MediaStream[]> AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles_TestData() - { - var data = new TheoryData<List<MediaStream>, string, int, string[], MediaStream[]>(); + private readonly SubtitleResolver _subtitleResolver; - var index = 0; - data.Add( - new List<MediaStream>(), - "/video/My.Video.mkv", - index, - new[] - { - "/video/My.Video.mp3", - "/video/My.Video.png", - "/video/My.Video.srt", - "/video/My.Video.txt", - "/video/My.Video.vtt", - "/video/My.Video.ass", - "/video/My.Video.sub", - "/video/My.Video.ssa", - "/video/My.Video.smi", - "/video/My.Video.sami", - "/video/My.Video.en.srt", - "/video/My.Video.default.en.srt", - "/video/My.Video.default.forced.en.srt", - "/video/My.Video.en.default.forced.srt", - "/video/My.Video.With.Additional.Garbage.en.srt", - "/video/My.Video With Additional Garbage.srt" - }, - new[] - { - CreateMediaStream("/video/My.Video.srt", "srt", null, index++), - CreateMediaStream("/video/My.Video.vtt", "vtt", null, index++), - CreateMediaStream("/video/My.Video.ass", "ass", null, index++), - CreateMediaStream("/video/My.Video.sub", "sub", null, index++), - CreateMediaStream("/video/My.Video.ssa", "ssa", null, index++), - CreateMediaStream("/video/My.Video.smi", "smi", null, index++), - CreateMediaStream("/video/My.Video.sami", "sami", null, index++), - CreateMediaStream("/video/My.Video.en.srt", "srt", "en", index++), - CreateMediaStream("/video/My.Video.default.en.srt", "srt", "en", index++, isDefault: true), - CreateMediaStream("/video/My.Video.default.forced.en.srt", "srt", "en", index++, isForced: true, isDefault: true), - CreateMediaStream("/video/My.Video.en.default.forced.srt", "srt", "en", index++, isForced: true, isDefault: true), - CreateMediaStream("/video/My.Video.With.Additional.Garbage.en.srt", "srt", "en", index), - }); + public SubtitleResolverTests() + { + // prep BaseItem and Video for calls made that expect managers + Video.LiveTvManager = Mock.Of<ILiveTvManager>(); - return data; - } + var applicationPaths = new Mock<IServerApplicationPaths>().Object; + var serverConfig = new Mock<IServerConfigurationManager>(); + serverConfig.Setup(c => c.ApplicationPaths) + .Returns(applicationPaths); + BaseItem.ConfigurationManager = serverConfig.Object; - [Theory] - [MemberData(nameof(AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles_TestData))] - public void AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles(List<MediaStream> streams, string videoPath, int startIndex, string[] files, MediaStream[] expectedResult) - { - new SubtitleResolver(Mock.Of<ILocalizationManager>()).AddExternalSubtitleStreams(streams, videoPath, startIndex, files); + // build resolver to test with + var localizationManager = Mock.Of<ILocalizationManager>(); - Assert.Equal(expectedResult.Length, streams.Count); - for (var i = 0; i < expectedResult.Length; i++) + var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict); + mediaEncoder.Setup(me => me.GetMediaInfo(It.IsAny<MediaInfoRequest>(), It.IsAny<CancellationToken>())) + .Returns<MediaInfoRequest, CancellationToken>((_, _) => Task.FromResult(new MediaBrowser.Model.MediaInfo.MediaInfo { - var expected = expectedResult[i]; - var actual = streams[i]; + MediaStreams = new List<MediaStream> + { + new() + } + })); - Assert.Equal(expected.Index, actual.Index); - Assert.Equal(expected.Type, actual.Type); - Assert.Equal(expected.IsExternal, actual.IsExternal); - Assert.Equal(expected.Path, actual.Path); - Assert.Equal(expected.IsDefault, actual.IsDefault); - Assert.Equal(expected.IsForced, actual.IsForced); - Assert.Equal(expected.Language, actual.Language); - } - } + var fileSystem = new Mock<IFileSystem>(MockBehavior.Strict); + fileSystem.Setup(fs => fs.DirectoryExists(It.IsRegex(MediaInfoResolverTests.VideoDirectoryRegex))) + .Returns(true); + fileSystem.Setup(fs => fs.DirectoryExists(It.IsRegex(MediaInfoResolverTests.MetadataDirectoryRegex))) + .Returns(true); + + _subtitleResolver = new SubtitleResolver(localizationManager, mediaEncoder.Object, fileSystem.Object, new NamingOptions()); + } + + [Theory] + [InlineData("My.Video.srt", false, true)] + [InlineData("My.Video.mp3", false, false)] + [InlineData("My.Video.srt", true, true)] + [InlineData("My.Video.mp3", true, false)] + public async void GetExternalStreams_MixedFilenames_PicksSubtitles(string file, bool metadataDirectory, bool matches) + { + BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>(); - [Theory] - [InlineData("/video/My Video.mkv", "/video/My Video.srt", "srt", null, false, false)] - [InlineData("/video/My.Video.mkv", "/video/My.Video.srt", "srt", null, false, false)] - [InlineData("/video/My.Video.mkv", "/video/My.Video.foreign.srt", "srt", null, true, false)] - [InlineData("/video/My Video.mkv", "/video/My Video.forced.srt", "srt", null, true, false)] - [InlineData("/video/My.Video.mkv", "/video/My.Video.default.srt", "srt", null, false, true)] - [InlineData("/video/My.Video.mkv", "/video/My.Video.forced.default.srt", "srt", null, true, true)] - [InlineData("/video/My.Video.mkv", "/video/My.Video.en.srt", "srt", "en", false, false)] - [InlineData("/video/My.Video.mkv", "/video/My.Video.default.en.srt", "srt", "en", false, true)] - [InlineData("/video/My.Video.mkv", "/video/My.Video.default.forced.en.srt", "srt", "en", true, true)] - [InlineData("/video/My.Video.mkv", "/video/My.Video.en.default.forced.srt", "srt", "en", true, true)] - public void AddExternalSubtitleStreams_GivenSingleFile_ReturnsExpectedSubtitle(string videoPath, string file, string codec, string? language, bool isForced, bool isDefault) + var video = new Movie { - var streams = new List<MediaStream>(); - var expected = CreateMediaStream(file, codec, language, 0, isForced, isDefault); + Path = MediaInfoResolverTests.VideoDirectoryPath + "/My.Video.mkv" + }; - new SubtitleResolver(Mock.Of<ILocalizationManager>()).AddExternalSubtitleStreams(streams, videoPath, 0, new[] { file }); + var directoryService = MediaInfoResolverTests.GetDirectoryServiceForExternalFile(file, metadataDirectory); + var streams = await _subtitleResolver.GetExternalStreamsAsync(video, 0, directoryService, false, CancellationToken.None); + if (matches) + { Assert.Single(streams); - var actual = streams[0]; - - Assert.Equal(expected.Index, actual.Index); - Assert.Equal(expected.Type, actual.Type); - Assert.Equal(expected.IsExternal, actual.IsExternal); - Assert.Equal(expected.Path, actual.Path); - Assert.Equal(expected.IsDefault, actual.IsDefault); - Assert.Equal(expected.IsForced, actual.IsForced); - Assert.Equal(expected.Language, actual.Language); + Assert.Equal(MediaStreamType.Subtitle, actual.Type); } - - private static MediaStream CreateMediaStream(string path, string codec, string? language, int index, bool isForced = false, bool isDefault = false) + else { - return new() - { - Index = index, - Codec = codec, - Type = MediaStreamType.Subtitle, - IsExternal = true, - Path = path, - IsDefault = isDefault, - IsForced = isForced, - Language = language - }; + Assert.Empty(streams); } } } diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs index 1503a3392..7e88cdb20 100644 --- a/tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs @@ -116,9 +116,9 @@ namespace Jellyfin.Providers.Tests.MediaInfo } var mediaSourceManager = new Mock<IMediaSourceManager>(MockBehavior.Strict); - mediaSourceManager.Setup(i => i.GetMediaStreams(It.Is<MediaStreamQuery>(q => q.ItemId == item.Id && q.Index == item.DefaultVideoStreamIndex))) + mediaSourceManager.Setup(i => i.GetMediaStreams(It.Is<MediaStreamQuery>(q => q.ItemId.Equals(item.Id) && q.Index == item.DefaultVideoStreamIndex))) .Returns(defaultStreamList); - mediaSourceManager.Setup(i => i.GetMediaStreams(It.Is<MediaStreamQuery>(q => q.ItemId == item.Id && q.Type == MediaStreamType.Video))) + mediaSourceManager.Setup(i => i.GetMediaStreams(It.Is<MediaStreamQuery>(q => q.ItemId.Equals(item.Id) && q.Type == MediaStreamType.Video))) .Returns(mediaStreams); return mediaSourceManager.Object; } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index 3146f277f..2d0a7c031 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -21,8 +21,8 @@ <ItemGroup> <PackageReference Include="AutoFixture" Version="4.17.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> - <PackageReference Include="Moq" Version="4.16.1" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" /> + <PackageReference Include="Moq" Version="4.17.2" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="Xunit.SkippableFact" Version="1.4.13" /> @@ -36,7 +36,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs index 5c7c983c2..c21871297 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs @@ -1,4 +1,4 @@ -using Emby.Naming.Common; +using Emby.Naming.Common; using Emby.Server.Implementations.Library.Resolvers.TV; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; @@ -7,6 +7,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; using Moq; using Xunit; @@ -21,7 +22,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library { var parent = new Folder { Name = "extras" }; - var episodeResolver = new EpisodeResolver(_namingOptions); + var episodeResolver = new EpisodeResolver(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions); var itemResolveArgs = new ItemResolveArgs( Mock.Of<IServerApplicationPaths>(), Mock.Of<IDirectoryService>()) @@ -44,7 +45,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library // Have to create a mock because of moq proxies not being castable to a concrete implementation // https://github.com/jellyfin/jellyfin/blob/ab0cff8556403e123642dc9717ba778329554634/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs#L48 - var episodeResolver = new EpisodeResolverMock(_namingOptions); + var episodeResolver = new EpisodeResolverMock(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions); var itemResolveArgs = new ItemResolveArgs( Mock.Of<IServerApplicationPaths>(), Mock.Of<IDirectoryService>()) @@ -61,7 +62,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library private class EpisodeResolverMock : EpisodeResolver { - public EpisodeResolverMock(NamingOptions namingOptions) : base(namingOptions) + public EpisodeResolverMock(ILogger<EpisodeResolver> logger, NamingOptions namingOptions) : base(logger, namingOptions) { } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs index f5c8cc970..599599071 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs index f2efcddba..efc3ac0c2 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs @@ -5,6 +5,7 @@ using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; using Moq; using Xunit; @@ -17,7 +18,7 @@ public class MovieResolverTests [Fact] public void Resolve_GivenLocalAlternateVersion_ResolvesToVideo() { - var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), _namingOptions); + var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions); var itemResolveArgs = new ItemResolveArgs( Mock.Of<IServerApplicationPaths>(), Mock.Of<IDirectoryService>()) diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs index 3396a94e5..0afb6f88d 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs @@ -14,6 +14,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { private readonly JellyfinApplicationFactory _factory; private readonly JsonSerializerOptions _jsonOpions = JsonDefaults.Options; + private static string? _accessToken; public DashboardControllerTests(JellyfinApplicationFactory factory) { @@ -57,6 +58,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task GetConfigurationPages_NoParams_AllConfigurationPages() { var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); var response = await client.GetAsync("/web/ConfigurationPages").ConfigureAwait(false); @@ -71,6 +73,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task GetConfigurationPages_True_MainMenuConfigurationPages() { var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); var response = await client.GetAsync("/web/ConfigurationPages?enableInMainMenu=true").ConfigureAwait(false); diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs index 588e25a82..9d34c39a2 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs @@ -130,7 +130,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers var users = await JsonSerializer.DeserializeAsync<UserDto[]>( await client.GetStreamAsync("Users").ConfigureAwait(false), _jsonOpions).ConfigureAwait(false); - var user = users!.First(x => x.Id == _testUserId); + var user = users!.First(x => x.Id.Equals(_testUserId)); Assert.True(user.HasPassword); Assert.True(user.HasConfiguredPassword); } @@ -153,7 +153,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers var users = await JsonSerializer.DeserializeAsync<UserDto[]>( await client.GetStreamAsync("Users").ConfigureAwait(false), _jsonOpions).ConfigureAwait(false); - var user = users!.First(x => x.Id == _testUserId); + var user = users!.First(x => x.Id.Equals(_testUserId)); Assert.False(user.HasPassword); Assert.False(user.HasConfiguredPassword); } diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj index 08eea4b15..8e0a208af 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj +++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj @@ -9,14 +9,14 @@ <PackageReference Include="AutoFixture" Version="4.17.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" /> - <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.2" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.3" /> <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="Xunit.Priority" Version="1.1.6" /> <PackageReference Include="coverlet.collector" Version="3.1.2" /> - <PackageReference Include="Moq" Version="4.16.1" /> + <PackageReference Include="Moq" Version="4.17.2" /> </ItemGroup> <ItemGroup> @@ -33,7 +33,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj index 2ab32d6f6..90bd74bd9 100644 --- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj +++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj @@ -10,13 +10,13 @@ <PackageReference Include="AutoFixture" Version="4.17.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" /> - <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.2" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.3" /> <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="3.1.2" /> - <PackageReference Include="Moq" Version="4.16.1" /> + <PackageReference Include="Moq" Version="4.17.2" /> </ItemGroup> <!-- Code Analyzers --> @@ -26,7 +26,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj index 1bb2115cc..d8bc4d596 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj +++ b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj @@ -13,8 +13,8 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> - <PackageReference Include="Moq" Version="4.16.1" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" /> + <PackageReference Include="Moq" Version="4.17.2" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="3.1.2" /> @@ -27,7 +27,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> |
