aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2023-03-16 11:07:22 +0100
committerShadowghost <Ghost_of_Stone@web.de>2023-03-16 11:07:22 +0100
commit520c07e8cad3e4372f6a5214160d1600106a7bfd (patch)
tree1a50b779de47026ed6bd6b4344eb019e13aa3bdc
parent80b8661008f271efad595e75de7b0c50971b131b (diff)
parent24cc1e9aead3a353b4749dc3e0c0d17a79a85c96 (diff)
Merge branch 'master' into network-rewrite
-rw-r--r--.github/workflows/codeql-analysis.yml8
-rw-r--r--.github/workflows/commands.yml4
-rw-r--r--.github/workflows/openapi.yml4
-rw-r--r--Directory.Packages.props26
-rw-r--r--Dockerfile1
-rw-r--r--Dockerfile.arm1
-rw-r--r--Dockerfile.arm641
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs90
-rw-r--r--Emby.Dlna/PlayTo/PlayToManager.cs5
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs4
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs24
-rw-r--r--Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs18
-rw-r--r--Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs13
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs8
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs3
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs7
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs15
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs23
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs8
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs6
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs13
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs18
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs6
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs32
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs8
-rw-r--r--Emby.Server.Implementations/Localization/Core/is.json11
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs80
-rw-r--r--Jellyfin.Api/Controllers/ChannelsController.cs6
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs12
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs5
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs10
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs14
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj1
-rw-r--r--Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs4
-rw-r--r--MediaBrowser.Controller/Channels/IChannelManager.cs4
-rw-r--r--MediaBrowser.Controller/Entities/AggregateFolder.cs2
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs2
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs2
-rw-r--r--MediaBrowser.Controller/Library/ItemResolveArgs.cs25
-rw-r--r--MediaBrowser.Controller/LiveTv/ILiveTvManager.cs4
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs162
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs30
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs5
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs123
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs187
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs68
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs29
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs169
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj1
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs13
-rw-r--r--MediaBrowser.Model/Configuration/EncodingOptions.cs3
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs10
-rw-r--r--MediaBrowser.Model/Dlna/ConditionProcessor.cs39
-rw-r--r--MediaBrowser.Model/Dlna/ContainerProfile.cs2
-rw-r--r--MediaBrowser.Model/Dlna/DirectPlayProfile.cs6
-rw-r--r--MediaBrowser.Model/Dlna/ITranscoderSupport.cs18
-rw-r--r--MediaBrowser.Model/Dlna/MediaOptions.cs10
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs146
-rw-r--r--MediaBrowser.Model/Dlna/StreamInfo.cs5
-rw-r--r--MediaBrowser.Model/Globalization/ILocalizationManager.cs3
-rw-r--r--MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs41
-rw-r--r--MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs14
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs206
-rw-r--r--MediaBrowser.Providers/MediaInfo/ProbeProvider.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs107
-rw-r--r--deployment/Dockerfile.centos.amd642
-rw-r--r--deployment/Dockerfile.fedora.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.arm642
-rw-r--r--deployment/Dockerfile.ubuntu.armhf2
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs4
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs24
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs76
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs10
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs4
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs4
78 files changed, 1551 insertions, 515 deletions
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 6d87af538..d5dcb785c 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+ uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3
- name: Setup .NET
uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
with:
dotnet-version: '7.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2
+ uses: github/codeql-action/init@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2
+ uses: github/codeql-action/autobuild@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2
+ uses: github/codeql-action/analyze@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index 75227c57b..c0b365b02 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -24,7 +24,7 @@ jobs:
reactions: '+1'
- name: Checkout the latest code
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+ uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@@ -51,7 +51,7 @@ jobs:
reactions: eyes
- name: Checkout the latest code
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+ uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml
index aa2e0417f..a67ce90d2 100644
--- a/.github/workflows/openapi.yml
+++ b/.github/workflows/openapi.yml
@@ -14,7 +14,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+ uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -39,7 +39,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+ uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 270ea2f17..d6e055899 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -17,30 +17,30 @@
<PackageVersion Include="Diacritics" Version="3.3.14" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
- <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.5" />
+ <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.6" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.5" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
- <PackageVersion Include="libse" Version="3.6.10" />
- <PackageVersion Include="LrcParser" Version="2022.529.1" />
+ <PackageVersion Include="libse" Version="3.6.11" />
+ <PackageVersion Include="LrcParser" Version="2023.308.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
- <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.3" />
+ <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
- <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.3" />
+ <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.3" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.3" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.3" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.3" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.4" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.4" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.4" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.3" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.4" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.4" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
@@ -52,7 +52,7 @@
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="NEbml" Version="0.11.0" />
- <PackageVersion Include="Newtonsoft.Json" Version="13.0.2" />
+ <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="PlaylistsNET" Version="1.3.1" />
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.0.0" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
diff --git a/Dockerfile b/Dockerfile
index f5f5787be..e51d285e1 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -10,6 +10,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
&& npm ci --no-audit --unsafe-perm \
+ && npm run build:production \
&& mv dist /dist
FROM debian:stable-slim as app
diff --git a/Dockerfile.arm b/Dockerfile.arm
index bbb84a461..46a3e9b99 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -11,6 +11,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
&& npm ci --no-audit --unsafe-perm \
+ && npm run build:production \
&& mv dist /dist
FROM multiarch/qemu-user-static:x86_64-arm as qemu
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index 5572586ae..4f9d5e1fd 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -11,6 +11,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
&& npm ci --no-audit --unsafe-perm \
+ && npm run build:production \
&& mv dist /dist
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 7b1f942c5..86db36337 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -66,7 +64,8 @@ namespace Emby.Dlna.PlayTo
IUserDataManager userDataManager,
ILocalizationManager localization,
IMediaSourceManager mediaSourceManager,
- IMediaEncoder mediaEncoder)
+ IMediaEncoder mediaEncoder,
+ Device device)
{
_session = session;
_sessionManager = sessionManager;
@@ -82,14 +81,7 @@ namespace Emby.Dlna.PlayTo
_localization = localization;
_mediaSourceManager = mediaSourceManager;
_mediaEncoder = mediaEncoder;
- }
-
- public bool IsSessionActive => !_disposed && _device is not null;
- public bool SupportsMediaControl => IsSessionActive;
-
- public void Init(Device device)
- {
_device = device;
_device.OnDeviceUnavailable = OnDeviceUnavailable;
_device.PlaybackStart += OnDevicePlaybackStart;
@@ -102,6 +94,10 @@ namespace Emby.Dlna.PlayTo
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
}
+ public bool IsSessionActive => !_disposed;
+
+ public bool SupportsMediaControl => IsSessionActive;
+
/*
* Send a message to the DLNA device to notify what is the next track in the playlist.
*/
@@ -131,22 +127,22 @@ namespace Emby.Dlna.PlayTo
}
}
- private void OnDeviceDiscoveryDeviceLeft(object sender, GenericEventArgs<UpnpDeviceInfo> e)
+ private void OnDeviceDiscoveryDeviceLeft(object? sender, GenericEventArgs<UpnpDeviceInfo> e)
{
var info = e.Argument;
if (!_disposed
- && info.Headers.TryGetValue("USN", out string usn)
+ && info.Headers.TryGetValue("USN", out string? usn)
&& usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1
&& (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1
- || (info.Headers.TryGetValue("NT", out string nt)
+ || (info.Headers.TryGetValue("NT", out string? nt)
&& nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)))
{
OnDeviceUnavailable();
}
}
- private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
+ private async void OnDeviceMediaChanged(object? sender, MediaChangedEventArgs e)
{
if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
{
@@ -188,7 +184,7 @@ namespace Emby.Dlna.PlayTo
}
}
- private async void OnDevicePlaybackStopped(object sender, PlaybackStoppedEventArgs e)
+ private async void OnDevicePlaybackStopped(object? sender, PlaybackStoppedEventArgs e)
{
if (_disposed)
{
@@ -257,7 +253,7 @@ namespace Emby.Dlna.PlayTo
}
}
- private async void OnDevicePlaybackStart(object sender, PlaybackStartEventArgs e)
+ private async void OnDevicePlaybackStart(object? sender, PlaybackStartEventArgs e)
{
if (_disposed)
{
@@ -281,7 +277,7 @@ namespace Emby.Dlna.PlayTo
}
}
- private async void OnDevicePlaybackProgress(object sender, PlaybackProgressEventArgs e)
+ private async void OnDevicePlaybackProgress(object? sender, PlaybackProgressEventArgs e)
{
if (_disposed)
{
@@ -486,9 +482,9 @@ namespace Emby.Dlna.PlayTo
private PlaylistItem CreatePlaylistItem(
BaseItem item,
- User user,
+ User? user,
long startPostionTicks,
- string mediaSourceId,
+ string? mediaSourceId,
int? audioStreamIndex,
int? subtitleStreamIndex)
{
@@ -525,7 +521,7 @@ namespace Emby.Dlna.PlayTo
return playlistItem;
}
- private string GetDlnaHeaders(PlaylistItem item)
+ private string? GetDlnaHeaders(PlaylistItem item)
{
var profile = item.Profile;
var streamInfo = item.StreamInfo;
@@ -579,7 +575,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
- private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
+ private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string? mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
{
if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
{
@@ -696,7 +692,6 @@ namespace Emby.Dlna.PlayTo
_device.MediaChanged -= OnDeviceMediaChanged;
_deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
_device.OnDeviceUnavailable = null;
- _device = null;
_disposed = true;
}
@@ -716,7 +711,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.ToggleMute:
return _device.ToggleMute(cancellationToken);
case GeneralCommandType.SetAudioStreamIndex:
- if (command.Arguments.TryGetValue("Index", out string index))
+ if (command.Arguments.TryGetValue("Index", out string? index))
{
if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
@@ -740,7 +735,7 @@ namespace Emby.Dlna.PlayTo
throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
case GeneralCommandType.SetVolume:
- if (command.Arguments.TryGetValue("Volume", out string vol))
+ if (command.Arguments.TryGetValue("Volume", out string? vol))
{
if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
{
@@ -865,34 +860,19 @@ namespace Emby.Dlna.PlayTo
throw new ObjectDisposedException(GetType().Name);
}
- if (_device is null)
- {
- return Task.CompletedTask;
- }
-
- if (name == SessionMessageType.Play)
- {
- return SendPlayCommand(data as PlayRequest, cancellationToken);
- }
-
- if (name == SessionMessageType.Playstate)
+ return name switch
{
- return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
- }
-
- if (name == SessionMessageType.GeneralCommand)
- {
- return SendGeneralCommand(data as GeneralCommand, cancellationToken);
- }
-
- // Not supported or needed right now
- return Task.CompletedTask;
+ SessionMessageType.Play => SendPlayCommand((data as PlayRequest)!, cancellationToken),
+ SessionMessageType.Playstate => SendPlaystateCommand((data as PlaystateRequest)!, cancellationToken),
+ SessionMessageType.GeneralCommand => SendGeneralCommand((data as GeneralCommand)!, cancellationToken),
+ _ => Task.CompletedTask // Not supported or needed right now
+ };
}
private class StreamParams
{
- private MediaSourceInfo _mediaSource;
- private IMediaSourceManager _mediaSourceManager;
+ private MediaSourceInfo? _mediaSource;
+ private IMediaSourceManager? _mediaSourceManager;
public Guid ItemId { get; set; }
@@ -904,17 +884,17 @@ namespace Emby.Dlna.PlayTo
public int? SubtitleStreamIndex { get; set; }
- public string DeviceProfileId { get; set; }
+ public string? DeviceProfileId { get; set; }
- public string DeviceId { get; set; }
+ public string? DeviceId { get; set; }
- public string MediaSourceId { get; set; }
+ public string? MediaSourceId { get; set; }
- public string LiveStreamId { get; set; }
+ public string? LiveStreamId { get; set; }
- public BaseItem Item { get; set; }
+ public BaseItem? Item { get; set; }
- public async Task<MediaSourceInfo> GetMediaSource(CancellationToken cancellationToken)
+ public async Task<MediaSourceInfo?> GetMediaSource(CancellationToken cancellationToken)
{
if (_mediaSource is not null)
{
@@ -944,8 +924,8 @@ namespace Emby.Dlna.PlayTo
{
var part = parts[i];
- if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
{
if (Guid.TryParse(parts[i + 1], out var result))
{
diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs
index b9bfad9d9..241dff5ae 100644
--- a/Emby.Dlna/PlayTo/PlayToManager.cs
+++ b/Emby.Dlna/PlayTo/PlayToManager.cs
@@ -205,12 +205,11 @@ namespace Emby.Dlna.PlayTo
_userDataManager,
_localization,
_mediaSourceManager,
- _mediaEncoder);
+ _mediaEncoder,
+ device);
sessionInfo.AddController(controller);
- controller.Init(device);
-
var profile = _dlnaManager.GetProfile(device.Properties.ToDeviceIdentification()) ??
_dlnaManager.GetDefaultProfile();
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 485253bf7..d1caa5263 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -80,11 +80,13 @@ using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Controller.SyncPlay;
using MediaBrowser.Controller.TV;
using MediaBrowser.LocalMetadata.Savers;
+using MediaBrowser.MediaEncoding.BdInfo;
using MediaBrowser.MediaEncoding.Subtitles;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.System;
@@ -529,6 +531,8 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
+ serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
+
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 1e3c4dea1..961e225e9 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -157,16 +157,16 @@ namespace Emby.Server.Implementations.Channels
}
/// <inheritdoc />
- public QueryResult<Channel> GetChannelsInternal(ChannelQuery query)
+ public async Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query)
{
var user = query.UserId.Equals(default)
? null
: _userManager.GetUserById(query.UserId);
- var channels = GetAllChannels()
- .Select(GetChannelEntity)
+ var channels = await GetAllChannelEntitiesAsync()
.OrderBy(i => i.SortName)
- .ToList();
+ .ToListAsync()
+ .ConfigureAwait(false);
if (query.IsRecordingsFolder.HasValue)
{
@@ -226,6 +226,7 @@ namespace Emby.Server.Implementations.Channels
if (user is not null)
{
+ var userId = user.Id.ToString("N", CultureInfo.InvariantCulture);
channels = channels.Where(i =>
{
if (!i.IsVisible(user))
@@ -235,7 +236,7 @@ namespace Emby.Server.Implementations.Channels
try
{
- return GetChannelProvider(i).IsEnabledFor(user.Id.ToString("N", CultureInfo.InvariantCulture));
+ return GetChannelProvider(i).IsEnabledFor(userId);
}
catch
{
@@ -258,7 +259,7 @@ namespace Emby.Server.Implementations.Channels
{
foreach (var item in all)
{
- RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
+ await RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).ConfigureAwait(false);
}
}
@@ -269,13 +270,13 @@ namespace Emby.Server.Implementations.Channels
}
/// <inheritdoc />
- public QueryResult<BaseItemDto> GetChannels(ChannelQuery query)
+ public async Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query)
{
var user = query.UserId.Equals(default)
? null
: _userManager.GetUserById(query.UserId);
- var internalResult = GetChannelsInternal(query);
+ var internalResult = await GetChannelsInternalAsync(query).ConfigureAwait(false);
var dtoOptions = new DtoOptions();
@@ -327,9 +328,12 @@ namespace Emby.Server.Implementations.Channels
progress.Report(100);
}
- private Channel GetChannelEntity(IChannel channel)
+ private async IAsyncEnumerable<Channel> GetAllChannelEntitiesAsync()
{
- return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).GetAwaiter().GetResult();
+ foreach (IChannel channel in GetAllChannels())
+ {
+ yield return GetChannel(GetInternalChannelId(channel.Name)) ?? await GetChannel(channel, CancellationToken.None).ConfigureAwait(false);
+ }
}
private MediaSourceInfo[] GetSavedMediaSources(BaseItem item)
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index 05d0a9b79..2e3988f9e 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -276,25 +276,31 @@ namespace Emby.Server.Implementations.EntryPoints
/// Libraries the update timer callback.
/// </summary>
/// <param name="state">The state.</param>
- private void LibraryUpdateTimerCallback(object state)
+ private async void LibraryUpdateTimerCallback(object state)
{
+ List<Folder> foldersAddedTo;
+ List<Folder> foldersRemovedFrom;
+ List<BaseItem> itemsUpdated;
+ List<BaseItem> itemsAdded;
+ List<BaseItem> itemsRemoved;
lock (_libraryChangedSyncLock)
{
// Remove dupes in case some were saved multiple times
- var foldersAddedTo = _foldersAddedTo
+ foldersAddedTo = _foldersAddedTo
.DistinctBy(x => x.Id)
.ToList();
- var foldersRemovedFrom = _foldersRemovedFrom
+ foldersRemovedFrom = _foldersRemovedFrom
.DistinctBy(x => x.Id)
.ToList();
- var itemsUpdated = _itemsUpdated
+ itemsUpdated = _itemsUpdated
.Where(i => !_itemsAdded.Contains(i))
.DistinctBy(x => x.Id)
.ToList();
- SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None).GetAwaiter().GetResult();
+ itemsAdded = _itemsAdded.ToList();
+ itemsRemoved = _itemsRemoved.ToList();
if (LibraryUpdateTimer is not null)
{
@@ -308,6 +314,8 @@ namespace Emby.Server.Implementations.EntryPoints
_foldersAddedTo.Clear();
_foldersRemovedFrom.Clear();
}
+
+ await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false);
}
/// <summary>
diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
index e724618b3..d32759017 100644
--- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
@@ -87,29 +87,30 @@ namespace Emby.Server.Implementations.EntryPoints
}
}
- private void UpdateTimerCallback(object? state)
+ private async void UpdateTimerCallback(object? state)
{
+ List<KeyValuePair<Guid, List<BaseItem>>> changes;
lock (_syncLock)
{
// Remove dupes in case some were saved multiple times
- var changes = _changedItems.ToList();
+ changes = _changedItems.ToList();
_changedItems.Clear();
- SendNotifications(changes, CancellationToken.None).GetAwaiter().GetResult();
-
if (_updateTimer is not null)
{
_updateTimer.Dispose();
_updateTimer = null;
}
}
+
+ await SendNotifications(changes, CancellationToken.None).ConfigureAwait(false);
}
private async Task SendNotifications(List<KeyValuePair<Guid, List<BaseItem>>> changes, CancellationToken cancellationToken)
{
- foreach (var pair in changes)
+ foreach ((var key, var value) in changes)
{
- await SendNotifications(pair.Key, pair.Value, cancellationToken).ConfigureAwait(false);
+ await SendNotifications(key, value, cancellationToken).ConfigureAwait(false);
}
}
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 66bd68ddd..e5c520ca2 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -113,6 +113,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="imageProcessor">The image processor.</param>
/// <param name="memoryCache">The memory cache.</param>
/// <param name="namingOptions">The naming options.</param>
+ /// <param name="directoryService">The directory service.</param>
public LibraryManager(
IServerApplicationHost appHost,
ILoggerFactory loggerFactory,
@@ -128,7 +129,8 @@ namespace Emby.Server.Implementations.Library
IItemRepository itemRepository,
IImageProcessor imageProcessor,
IMemoryCache memoryCache,
- NamingOptions namingOptions)
+ NamingOptions namingOptions,
+ IDirectoryService directoryService)
{
_appHost = appHost;
_logger = loggerFactory.CreateLogger<LibraryManager>();
@@ -146,7 +148,7 @@ namespace Emby.Server.Implementations.Library
_memoryCache = memoryCache;
_namingOptions = namingOptions;
- _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions);
+ _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@@ -537,7 +539,7 @@ namespace Emby.Server.Implementations.Library
collectionType = GetContentTypeOverride(fullPath, true);
}
- var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService)
+ var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, this)
{
Parent = parent,
FileInfo = fileInfo,
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
index 69e905798..a74f82475 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
@@ -192,7 +192,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
continue;
}
- if (resolvedItem.Files.Count == 0)
+ // Until multi-part books are handled letting files stack hides them from browsing in the client
+ if (resolvedItem.Files.Count == 0 || resolvedItem.Extras.Count > 0 || resolvedItem.AlternateVersions.Count > 0)
{
continue;
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
index a922e3685..bbc70701c 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
@@ -25,16 +25,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
{
private readonly ILogger<MusicAlbumResolver> _logger;
private readonly NamingOptions _namingOptions;
+ private readonly IDirectoryService _directoryService;
/// <summary>
/// Initializes a new instance of the <see cref="MusicAlbumResolver"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param>
- public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions)
+ /// <param name="directoryService">The directory service.</param>
+ public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
{
_logger = logger;
_namingOptions = namingOptions;
+ _directoryService = directoryService;
}
/// <summary>
@@ -109,7 +112,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
}
// If args contains music it's a music album
- if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService))
+ if (ContainsMusic(args.FileSystemChildren, true, _directoryService))
{
return true;
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
index 2538c2b5b..c858dc53d 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
@@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Emby.Naming.Common;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
@@ -18,19 +19,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
public class MusicArtistResolver : ItemResolver<MusicArtist>
{
private readonly ILogger<MusicAlbumResolver> _logger;
- private NamingOptions _namingOptions;
+ private readonly NamingOptions _namingOptions;
+ private readonly IDirectoryService _directoryService;
/// <summary>
/// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
/// </summary>
/// <param name="logger">Instance of the <see cref="MusicAlbumResolver"/> interface.</param>
/// <param name="namingOptions">The <see cref="NamingOptions"/>.</param>
+ /// <param name="directoryService">The directory service.</param>
public MusicArtistResolver(
ILogger<MusicAlbumResolver> logger,
- NamingOptions namingOptions)
+ NamingOptions namingOptions,
+ IDirectoryService directoryService)
{
_logger = logger;
_namingOptions = namingOptions;
+ _directoryService = directoryService;
}
/// <summary>
@@ -78,9 +83,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
return null;
}
- var directoryService = args.DirectoryService;
-
- var albumResolver = new MusicAlbumResolver(_logger, _namingOptions);
+ var albumResolver = new MusicAlbumResolver(_logger, _namingOptions, _directoryService);
var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
@@ -97,7 +100,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
}
// If we contain a music album assume we are an artist folder
- if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, directoryService))
+ if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, _directoryService))
{
// Stop once we see a music album
state.Stop();
diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
index e8615e7db..4fac91bf1 100644
--- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -25,14 +25,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
private readonly ILogger _logger;
- protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions)
+ protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService)
{
_logger = logger;
NamingOptions = namingOptions;
+ DirectoryService = directoryService;
}
protected NamingOptions NamingOptions { get; }
+ protected IDirectoryService DirectoryService { get; }
+
/// <summary>
/// Resolves the specified args.
/// </summary>
@@ -65,13 +68,25 @@ namespace Emby.Server.Implementations.Library.Resolvers
var filename = child.Name;
if (child.IsDirectory)
{
- if (IsDvdDirectory(child.FullName, filename, args.DirectoryService))
+ if (IsDvdDirectory(child.FullName, filename, DirectoryService))
{
- videoType = VideoType.Dvd;
+ var videoTmp = new TVideoType
+ {
+ Path = args.Path,
+ VideoType = VideoType.Dvd
+ };
+ Set3DFormat(videoTmp);
+ return videoTmp;
}
else if (IsBluRayDirectory(filename))
{
- videoType = VideoType.BluRay;
+ var videoTmp = new TVideoType
+ {
+ Path = args.Path,
+ VideoType = VideoType.BluRay
+ };
+ Set3DFormat(videoTmp);
+ return videoTmp;
}
}
else if (IsDvdFile(filename))
diff --git a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
index 30c52e19d..0b255f673 100644
--- a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
@@ -4,6 +4,7 @@ using System.IO;
using Emby.Naming.Common;
using Emby.Naming.Video;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
@@ -25,11 +26,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="namingOptions">An instance of <see cref="NamingOptions"/>.</param>
- public ExtraResolver(ILogger<ExtraResolver> logger, NamingOptions namingOptions)
+ /// <param name="directoryService">The directory service.</param>
+ public ExtraResolver(ILogger<ExtraResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
{
_namingOptions = namingOptions;
- _trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions) };
- _videoResolvers = new IItemResolver[] { new GenericVideoResolver<Video>(logger, namingOptions) };
+ _trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions, directoryService) };
+ _videoResolvers = new IItemResolver[] { new GenericVideoResolver<Video>(logger, namingOptions, directoryService) };
}
/// <summary>
diff --git a/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs
index 5e33b402d..ba320266a 100644
--- a/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs
@@ -2,6 +2,7 @@
using Emby.Naming.Common;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Resolvers
@@ -18,8 +19,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param>
- public GenericVideoResolver(ILogger logger, NamingOptions namingOptions)
- : base(logger, namingOptions)
+ /// <param name="directoryService">The directory service.</param>
+ public GenericVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService)
+ : base(logger, namingOptions, directoryService)
{
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index ef4fa1fd2..ea980b992 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -43,8 +43,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// <param name="imageProcessor">The image processor.</param>
/// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param>
- public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions)
- : base(logger, namingOptions)
+ /// <param name="directoryService">The directory service.</param>
+ public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
+ : base(logger, namingOptions, directoryService)
{
_imageProcessor = imageProcessor;
}
@@ -97,12 +98,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
- movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
+ movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false);
}
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
{
- movie = FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
+ movie = FindMovie<Video>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false);
}
if (string.IsNullOrEmpty(collectionType))
@@ -118,12 +119,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null;
}
- movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
+ movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true);
}
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
- movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
+ movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true);
}
// ignore extras
diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
index e11fb262e..9026160ff 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
@@ -1,7 +1,5 @@
#nullable disable
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -12,15 +10,20 @@ using Jellyfin.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
namespace Emby.Server.Implementations.Library.Resolvers
{
+ /// <summary>
+ /// Class PhotoResolver.
+ /// </summary>
public class PhotoResolver : ItemResolver<Photo>
{
private readonly IImageProcessor _imageProcessor;
private readonly NamingOptions _namingOptions;
+ private readonly IDirectoryService _directoryService;
private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
@@ -35,10 +38,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
"default"
};
- public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PhotoResolver"/> class.
+ /// </summary>
+ /// <param name="imageProcessor">The image processor.</param>
+ /// <param name="namingOptions">The naming options.</param>
+ /// <param name="directoryService">The directory service.</param>
+ public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions, IDirectoryService directoryService)
{
_imageProcessor = imageProcessor;
_namingOptions = namingOptions;
+ _directoryService = directoryService;
}
/// <summary>
@@ -61,7 +71,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
var filename = Path.GetFileNameWithoutExtension(args.Path);
// Make sure the image doesn't belong to a video file
- var files = args.DirectoryService.GetFiles(Path.GetDirectoryName(args.Path));
+ var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path));
foreach (var file in files)
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
index 0fcc5070b..392ee4c77 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
@@ -5,6 +5,7 @@ using System.Linq;
using Emby.Naming.Common;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
@@ -20,8 +21,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param>
- public EpisodeResolver(ILogger<EpisodeResolver> logger, NamingOptions namingOptions)
- : base(logger, namingOptions)
+ /// <param name="directoryService">The directory service.</param>
+ public EpisodeResolver(ILogger<EpisodeResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
+ : base(logger, namingOptions, directoryService)
{
}
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index 0e2d34d39..17f1d1905 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -111,10 +111,10 @@ namespace Emby.Server.Implementations.Library
if (query.IncludeExternalContent)
{
- var channelResult = _channelManager.GetChannelsInternal(new ChannelQuery
+ var channelResult = _channelManager.GetChannelsInternalAsync(new ChannelQuery
{
UserId = query.UserId
- });
+ }).GetAwaiter().GetResult();
var channels = channelResult.Items;
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index 4003468d0..ee039ff0f 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -1312,20 +1312,19 @@ namespace Emby.Server.Implementations.LiveTv
return 7;
}
- private QueryResult<BaseItem> GetEmbyRecordings(RecordingQuery query, DtoOptions dtoOptions, User user)
+ private async Task<QueryResult<BaseItem>> GetEmbyRecordingsAsync(RecordingQuery query, DtoOptions dtoOptions, User user)
{
if (user is null)
{
return new QueryResult<BaseItem>();
}
- var folderIds = GetRecordingFolders(user, true)
- .Select(i => i.Id)
- .ToList();
+ var folders = await GetRecordingFoldersAsync(user, true).ConfigureAwait(false);
+ var folderIds = Array.ConvertAll(folders, x => x.Id);
var excludeItemTypes = new List<BaseItemKind>();
- if (folderIds.Count == 0)
+ if (folderIds.Length == 0)
{
return new QueryResult<BaseItem>();
}
@@ -1392,7 +1391,7 @@ namespace Emby.Server.Implementations.LiveTv
{
MediaTypes = new[] { MediaType.Video },
Recursive = true,
- AncestorIds = folderIds.ToArray(),
+ AncestorIds = folderIds,
IsFolder = false,
IsVirtualItem = false,
Limit = limit,
@@ -1528,7 +1527,7 @@ namespace Emby.Server.Implementations.LiveTv
}
}
- public QueryResult<BaseItemDto> GetRecordings(RecordingQuery query, DtoOptions options)
+ public async Task<QueryResult<BaseItemDto>> GetRecordingsAsync(RecordingQuery query, DtoOptions options)
{
var user = query.UserId.Equals(default)
? null
@@ -1536,7 +1535,7 @@ namespace Emby.Server.Implementations.LiveTv
RemoveFields(options);
- var internalResult = GetEmbyRecordings(query, options, user);
+ var internalResult = await GetEmbyRecordingsAsync(query, options, user).ConfigureAwait(false);
var returnArray = _dtoService.GetBaseItemDtos(internalResult.Items, options, user);
@@ -2379,12 +2378,11 @@ namespace Emby.Server.Implementations.LiveTv
return _tvDtoService.GetInternalProgramId(externalId);
}
- public List<BaseItem> GetRecordingFolders(User user)
- {
- return GetRecordingFolders(user, false);
- }
+ /// <inheritdoc />
+ public Task<BaseItem[]> GetRecordingFoldersAsync(User user)
+ => GetRecordingFoldersAsync(user, false);
- private List<BaseItem> GetRecordingFolders(User user, bool refreshChannels)
+ private async Task<BaseItem[]> GetRecordingFoldersAsync(User user, bool refreshChannels)
{
var folders = EmbyTV.EmbyTV.Current.GetRecordingFolders()
.SelectMany(i => i.Locations)
@@ -2396,14 +2394,16 @@ namespace Emby.Server.Implementations.LiveTv
.OrderBy(i => i.SortName)
.ToList();
- folders.AddRange(_channelManager.GetChannelsInternal(new MediaBrowser.Model.Channels.ChannelQuery
+ var channels = await _channelManager.GetChannelsInternalAsync(new MediaBrowser.Model.Channels.ChannelQuery
{
UserId = user.Id,
IsRecordingsFolder = true,
RefreshLatestChannelItems = refreshChannels
- }).Items);
+ }).ConfigureAwait(false);
+
+ folders.AddRange(channels.Items);
- return folders.Cast<BaseItem>().ToList();
+ return folders.Cast<BaseItem>().ToArray();
}
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index 046be7c5c..f2020e05f 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -122,9 +122,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
var attributes = ParseExtInf(extInf, out string remaining);
extInf = remaining;
- if (attributes.TryGetValue("tvg-logo", out string value))
+ if (attributes.TryGetValue("tvg-logo", out string tvgLogo))
{
- channel.ImageUrl = value;
+ channel.ImageUrl = tvgLogo;
+ }
+ else if (attributes.TryGetValue("logo", out string logo))
+ {
+ channel.ImageUrl = logo;
}
if (attributes.TryGetValue("group-title", out string groupTitle))
diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json
index b262a8b42..a40f49506 100644
--- a/Emby.Server.Implementations/Localization/Core/is.json
+++ b/Emby.Server.Implementations/Localization/Core/is.json
@@ -107,5 +107,14 @@
"TasksApplicationCategory": "Forrit",
"TasksLibraryCategory": "Miðlasafn",
"TasksMaintenanceCategory": "Viðhald",
- "Default": "Sjálfgefið"
+ "Default": "Sjálfgefið",
+ "TaskCleanActivityLog": "Hreinsa athafnaskrá",
+ "TaskRefreshPeople": "Endurnýja fólk",
+ "TaskDownloadMissingSubtitles": "Sækja texta sem vantar",
+ "TaskOptimizeDatabase": "Fínstilla gagnagrunn",
+ "Undefined": "Óskilgreint",
+ "TaskCleanLogsDescription": "Eyðir færslu skrám sem eru meira en {0} gömul.",
+ "TaskCleanLogs": "Hreinsa færslu skrá",
+ "TaskDownloadMissingSubtitlesDescription": "Leitar á netinu að texta sem vantar miðað við uppsetningu lýsigagna.",
+ "HearingImpaired": "Heyrnarskertur"
}
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 6e2a33fd5..166b71b4a 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -184,10 +184,19 @@ namespace Emby.Server.Implementations.Localization
/// <inheritdoc />
public IEnumerable<ParentalRating> GetParentalRatings()
{
- var ratings = GetParentalRatingsDictionary().Values.ToList();
+ // Use server default language for ratings
+ // Fall back to empty list if there are no parental ratings for that language
+ var ratings = GetParentalRatingsDictionary()?.Values.ToList()
+ ?? new List<ParentalRating>();
- // Add common ratings to ensure them being available for selection.
+ // Add common ratings to ensure them being available for selection
// Based on the US rating system due to it being the main source of rating in the metadata providers
+ // Unrated
+ if (!ratings.Any(x => x.Value is null))
+ {
+ ratings.Add(new ParentalRating("Unrated", null));
+ }
+
// Minimum rating possible
if (!ratings.Any(x => x.Value == 0))
{
@@ -237,36 +246,26 @@ namespace Emby.Server.Implementations.Localization
/// <summary>
/// Gets the parental ratings dictionary.
/// </summary>
+ /// <param name="countryCode">The optional two letter ISO language string.</param>
/// <returns><see cref="Dictionary{String, ParentalRating}" />.</returns>
- private Dictionary<string, ParentalRating> GetParentalRatingsDictionary()
+ private Dictionary<string, ParentalRating>? GetParentalRatingsDictionary(string? countryCode = null)
{
- var countryCode = _configurationManager.Configuration.MetadataCountryCode;
-
- // Fall back to US ratings if no country code is specified or country code does not exist.
+ // Fallback to server default if no country code is specified.
if (string.IsNullOrEmpty(countryCode))
{
- countryCode = "us";
+ countryCode = _configurationManager.Configuration.MetadataCountryCode;
}
- return GetRatings(countryCode)
- ?? GetRatings("us")
- ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'");
- }
-
- /// <summary>
- /// Gets the ratings for a country.
- /// </summary>
- /// <param name="countryCode">The country code.</param>
- /// <returns>The ratings.</returns>
- private Dictionary<string, ParentalRating>? GetRatings(string countryCode)
- {
- _allParentalRatings.TryGetValue(countryCode, out var countryValue);
+ if (_allParentalRatings.TryGetValue(countryCode, out var countryValue))
+ {
+ return countryValue;
+ }
- return countryValue;
+ return null;
}
/// <inheritdoc />
- public int? GetRatingLevel(string rating)
+ public int? GetRatingLevel(string rating, string? countryCode = null)
{
ArgumentException.ThrowIfNullOrEmpty(rating);
@@ -280,32 +279,51 @@ namespace Emby.Server.Implementations.Localization
rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase);
rating = rating.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase);
- var ratingsDictionary = GetParentalRatingsDictionary();
-
- if (ratingsDictionary.TryGetValue(rating, out ParentalRating? value))
+ // Use rating system matching the language
+ if (!string.IsNullOrEmpty(countryCode))
+ {
+ var ratingsDictionary = GetParentalRatingsDictionary(countryCode);
+ if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value))
+ {
+ return value.Value;
+ }
+ }
+ else
{
- return value.Value;
+ // Fall back to server default language for ratings check
+ // If it has no ratings, use the US ratings
+ var ratingsDictionary = GetParentalRatingsDictionary() ?? GetParentalRatingsDictionary("us");
+ if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value))
+ {
+ return value.Value;
+ }
}
- // If we don't find anything check all ratings systems
+ // If we don't find anything, check all ratings systems
foreach (var dictionary in _allParentalRatings.Values)
{
- if (dictionary.TryGetValue(rating, out value))
+ if (dictionary.TryGetValue(rating, out var value))
{
return value.Value;
}
}
- // Try splitting by : to handle "Germany: FSK 18"
+ // Try splitting by : to handle "Germany: FSK-18"
if (rating.Contains(':', StringComparison.OrdinalIgnoreCase))
{
return GetRatingLevel(rating.AsSpan().RightPart(':').ToString());
}
- // Remove prefix country code to handle "DE-18"
+ // Handle prefix country code to handle "DE-18"
if (rating.Contains('-', StringComparison.OrdinalIgnoreCase))
{
- return GetRatingLevel(rating.AsSpan().RightPart('-').ToString());
+ var ratingSpan = rating.AsSpan();
+
+ // Extract culture from country prefix
+ var culture = FindLanguageInfo(ratingSpan.LeftPart('-').ToString());
+
+ // Check rating system of culture
+ return GetRatingLevel(ratingSpan.RightPart('-').ToString(), culture?.TwoLetterISOLanguageName);
}
return null;
diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index b5c4d8346..11c4ac376 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -52,7 +52,7 @@ public class ChannelsController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the channels.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetChannels(
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetChannels(
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
@@ -61,7 +61,7 @@ public class ChannelsController : BaseJellyfinApiController
[FromQuery] bool? isFavorite)
{
userId = RequestHelpers.GetUserId(User, userId);
- return _channelManager.GetChannels(new ChannelQuery
+ return await _channelManager.GetChannelsAsync(new ChannelQuery
{
Limit = limit,
StartIndex = startIndex,
@@ -69,7 +69,7 @@ public class ChannelsController : BaseJellyfinApiController
SupportsLatestItems = supportsLatestItems,
SupportsMediaDeletion = supportsMediaDeletion,
IsFavorite = isFavorite
- });
+ }).ConfigureAwait(false);
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 4d8b4de24..16c77a923 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -19,6 +19,8 @@ using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.MediaEncoding.Encoder;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
@@ -1654,8 +1656,8 @@ public class DynamicHlsController : BaseJellyfinApiController
startNumber.ToString(CultureInfo.InvariantCulture),
baseUrlParam,
isEventPlaylist ? "event" : "vod",
- outputTsArg,
- outputPath).Trim();
+ EncodingUtils.NormalizePath(outputTsArg),
+ EncodingUtils.NormalizePath(outputPath)).Trim();
}
/// <summary>
@@ -1840,7 +1842,11 @@ public class DynamicHlsController : BaseJellyfinApiController
// args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
// video processing filters.
- args += _encodingHelper.GetVideoProcessingFilterParam(state, _encodingOptions, codec);
+ var videoProcessParam = _encodingHelper.GetVideoProcessingFilterParam(state, _encodingOptions, codec);
+
+ var negativeMapArgs = _encodingHelper.GetNegativeMapArgsByFilters(state, videoProcessParam);
+
+ args = negativeMapArgs + args + videoProcessParam;
// -start_at_zero is necessary to use with -ss when seeking,
// otherwise the target position cannot be determined.
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index 9c7148241..ece053a9a 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -246,6 +246,11 @@ public class ItemUpdateController : BaseJellyfinApiController
episode.AirsBeforeSeasonNumber = request.AirsBeforeSeasonNumber;
}
+ if (request.Height is not null && item is LiveTvChannel channel)
+ {
+ channel.Height = request.Height.Value;
+ }
+
item.Tags = request.Tags;
if (request.Taglines is not null)
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 96fc91f93..267ba4afb 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -252,7 +252,7 @@ public class LiveTvController : BaseJellyfinApiController
[HttpGet("Recordings")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.LiveTvAccess)]
- public ActionResult<QueryResult<BaseItemDto>> GetRecordings(
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecordings(
[FromQuery] string? channelId,
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
@@ -278,7 +278,7 @@ public class LiveTvController : BaseJellyfinApiController
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- return _liveTvManager.GetRecordings(
+ return await _liveTvManager.GetRecordingsAsync(
new RecordingQuery
{
ChannelId = channelId,
@@ -299,7 +299,7 @@ public class LiveTvController : BaseJellyfinApiController
ImageTypeLimit = imageTypeLimit,
EnableImages = enableImages
},
- dtoOptions);
+ dtoOptions).ConfigureAwait(false);
}
/// <summary>
@@ -383,13 +383,13 @@ public class LiveTvController : BaseJellyfinApiController
[HttpGet("Recordings/Folders")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.LiveTvAccess)]
- public ActionResult<QueryResult<BaseItemDto>> GetRecordingFolders([FromQuery] Guid? userId)
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecordingFolders([FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
- var folders = _liveTvManager.GetRecordingFolders(user);
+ var folders = await _liveTvManager.GetRecordingFoldersAsync(user).ConfigureAwait(false);
var returnArray = _dtoService.GetBaseItemDtos(folders, new DtoOptions(), user);
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index cd8ac4982..f25a71869 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -323,6 +323,15 @@ public class TranscodingJobHelper : IDisposable
if (delete(job.Path!))
{
await DeletePartialStreamFiles(job.Path!, job.Type, 0, 1500).ConfigureAwait(false);
+ if (job.MediaSource?.VideoType == VideoType.Dvd || job.MediaSource?.VideoType == VideoType.BluRay)
+ {
+ var concatFilePath = Path.Join(_serverConfigurationManager.GetTranscodePath(), job.MediaSource.Id + ".concat");
+ if (File.Exists(concatFilePath))
+ {
+ _logger.LogInformation("Deleting ffmpeg concat configuration at {Path}", concatFilePath);
+ File.Delete(concatFilePath);
+ }
+ }
}
if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
@@ -524,7 +533,10 @@ public class TranscodingJobHelper : IDisposable
if (state.SubtitleStream is not 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.VideoType != VideoType.Dvd)
+ {
+ 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))
{
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index a8a44fd3e..6a0a4706b 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -22,6 +22,7 @@
<ItemGroup>
<ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
+ <ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" />
<ProjectReference Include="..\src\Jellyfin.MediaEncoding.Hls\Jellyfin.MediaEncoding.Hls.csproj" />
</ItemGroup>
diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
index 3eac81419..4a5e0ecd4 100644
--- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
@@ -56,8 +56,8 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi
base.Dispose(dispose);
}
- private void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
+ private async void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
{
- SendData(true).GetAwaiter().GetResult();
+ await SendData(true).ConfigureAwait(false);
}
}
diff --git a/MediaBrowser.Controller/Channels/IChannelManager.cs b/MediaBrowser.Controller/Channels/IChannelManager.cs
index e392a3493..8eb27888a 100644
--- a/MediaBrowser.Controller/Channels/IChannelManager.cs
+++ b/MediaBrowser.Controller/Channels/IChannelManager.cs
@@ -46,14 +46,14 @@ namespace MediaBrowser.Controller.Channels
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The channels.</returns>
- QueryResult<Channel> GetChannelsInternal(ChannelQuery query);
+ Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query);
/// <summary>
/// Gets the channels.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The channels.</returns>
- QueryResult<BaseItemDto> GetChannels(ChannelQuery query);
+ Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query);
/// <summary>
/// Gets the latest channel items.
diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs
index 08c622cde..d789033f1 100644
--- a/MediaBrowser.Controller/Entities/AggregateFolder.cs
+++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs
@@ -120,7 +120,7 @@ namespace MediaBrowser.Controller.Entities
var path = ContainingFolderPath;
- var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService)
+ var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, LibraryManager)
{
FileInfo = FileSystem.GetDirectoryInfo(path)
};
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index b8601cccd..8fe9cfa7f 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -47,7 +47,7 @@ namespace MediaBrowser.Controller.Entities
/// The supported image extensions.
/// </summary>
public static readonly string[] SupportedImageExtensions
- = new[] { ".png", ".jpg", ".jpeg", ".tbn", ".gif" };
+ = new[] { ".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif" };
private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions)
{
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index 5ac619d8f..095b261c0 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -288,7 +288,7 @@ namespace MediaBrowser.Controller.Entities
{
var path = ContainingFolderPath;
- var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService)
+ var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, LibraryManager)
{
FileInfo = FileSystem.GetDirectoryInfo(path),
Parent = GetParent() as Folder,
diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
index 01986d303..c70102167 100644
--- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs
+++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
@@ -1,12 +1,11 @@
#nullable disable
-#pragma warning disable CA1721, CA1819, CS1591
+#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.IO;
@@ -23,22 +22,20 @@ namespace MediaBrowser.Controller.Library
/// </summary>
private readonly IServerApplicationPaths _appPaths;
+ private readonly ILibraryManager _libraryManager;
private LibraryOptions _libraryOptions;
/// <summary>
/// Initializes a new instance of the <see cref="ItemResolveArgs" /> class.
/// </summary>
/// <param name="appPaths">The app paths.</param>
- /// <param name="directoryService">The directory service.</param>
- public ItemResolveArgs(IServerApplicationPaths appPaths, IDirectoryService directoryService)
+ /// <param name="libraryManager">The library manager.</param>
+ public ItemResolveArgs(IServerApplicationPaths appPaths, ILibraryManager libraryManager)
{
_appPaths = appPaths;
- DirectoryService = directoryService;
+ _libraryManager = libraryManager;
}
- // TODO remove dependencies as properties, they should be injected where it makes sense
- public IDirectoryService DirectoryService { get; }
-
/// <summary>
/// Gets or sets the file system children.
/// </summary>
@@ -47,7 +44,7 @@ namespace MediaBrowser.Controller.Library
public LibraryOptions LibraryOptions
{
- get => _libraryOptions ??= Parent is null ? new LibraryOptions() : BaseItem.LibraryManager.GetLibraryOptions(Parent);
+ get => _libraryOptions ??= Parent is null ? new LibraryOptions() : _libraryManager.GetLibraryOptions(Parent);
set => _libraryOptions = value;
}
@@ -231,21 +228,15 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Gets the configured content type for the path.
/// </summary>
- /// <remarks>
- /// This is subject to future refactoring as it relies on a static property in BaseItem.
- /// </remarks>
/// <returns>The configured content type.</returns>
public string GetConfiguredContentType()
{
- return BaseItem.LibraryManager.GetConfiguredContentType(Path);
+ return _libraryManager.GetConfiguredContentType(Path);
}
/// <summary>
/// Gets the file system children that do not hit the ignore file check.
/// </summary>
- /// <remarks>
- /// This is subject to future refactoring as it relies on a static property in BaseItem.
- /// </remarks>
/// <returns>The file system children that are not ignored.</returns>
public IEnumerable<FileSystemMetadata> GetActualFileSystemChildren()
{
@@ -253,7 +244,7 @@ namespace MediaBrowser.Controller.Library
for (var i = 0; i < numberOfChildren; i++)
{
var child = FileSystemChildren[i];
- if (BaseItem.LibraryManager.IgnoreFile(child, Parent))
+ if (_libraryManager.IgnoreFile(child, Parent))
{
continue;
}
diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
index 46bdca302..3b6a16dee 100644
--- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
+++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
@@ -97,7 +97,7 @@ namespace MediaBrowser.Controller.LiveTv
/// <param name="query">The query.</param>
/// <param name="options">The options.</param>
/// <returns>A recording.</returns>
- QueryResult<BaseItemDto> GetRecordings(RecordingQuery query, DtoOptions options);
+ Task<QueryResult<BaseItemDto>> GetRecordingsAsync(RecordingQuery query, DtoOptions options);
/// <summary>
/// Gets the timers.
@@ -308,6 +308,6 @@ namespace MediaBrowser.Controller.LiveTv
void AddInfoToRecordingDto(BaseItem item, BaseItemDto dto, ActiveRecordingInfo activeRecordingInfo, User user = null);
- List<BaseItem> GetRecordingFolders(User user);
+ Task<BaseItem[]> GetRecordingFoldersAsync(User user);
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index e02a932b1..5de57917e 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -43,6 +43,9 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _maxKerneli915Hang = new Version(6, 1, 3);
private readonly Version _minFixedKernel60i915Hang = new Version(6, 0, 18);
+ private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0);
+ private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
+
private static readonly string[] _videoProfilesH264 = new[]
{
"ConstrainedBaseline",
@@ -558,9 +561,12 @@ namespace MediaBrowser.Controller.MediaEncoding
public string GetInputPathArgument(EncodingJobInfo state)
{
- var mediaPath = state.MediaPath ?? string.Empty;
-
- return _mediaEncoder.GetInputArgument(mediaPath, state.MediaSource);
+ return state.MediaSource.VideoType switch
+ {
+ VideoType.Dvd => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistVobFiles(state.MediaPath, null).ToList(), state.MediaSource),
+ VideoType.BluRay => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistM2tsFiles(state.MediaPath).ToList(), state.MediaSource),
+ _ => _mediaEncoder.GetInputArgument(state.MediaPath, state.MediaSource)
+ };
}
/// <summary>
@@ -639,6 +645,26 @@ namespace MediaBrowser.Controller.MediaEncoding
deviceIndex);
}
+ private string GetVulkanDeviceArgs(int deviceIndex, string deviceName, string srcDeviceAlias, string alias)
+ {
+ alias ??= VulkanAlias;
+ deviceIndex = deviceIndex >= 0
+ ? deviceIndex
+ : 0;
+ var vendorOpts = string.IsNullOrEmpty(deviceName)
+ ? ":" + deviceIndex
+ : ":" + "\"" + deviceName + "\"";
+ var options = string.IsNullOrEmpty(srcDeviceAlias)
+ ? vendorOpts
+ : "@" + srcDeviceAlias;
+
+ return string.Format(
+ CultureInfo.InvariantCulture,
+ " -init_hw_device vulkan={0}{1}",
+ alias,
+ options);
+ }
+
private string GetOpenclDeviceArgs(int deviceIndex, string deviceVendorName, string srcDeviceAlias, string alias)
{
alias ??= OpenclAlias;
@@ -821,6 +847,12 @@ namespace MediaBrowser.Controller.MediaEncoding
args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias));
filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
}
+ else
+ {
+ // libplacebo wants an explicitly set vulkan filter device.
+ args.Append(GetVulkanDeviceArgs(0, null, VaapiAlias, VulkanAlias));
+ filterDevArgs = GetFilterHwDeviceArgs(VulkanAlias);
+ }
}
else
{
@@ -962,8 +994,18 @@ namespace MediaBrowser.Controller.MediaEncoding
arg.Append(canvasArgs);
}
- arg.Append(" -i ")
- .Append(GetInputPathArgument(state));
+ if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay)
+ {
+ var tmpConcatPath = Path.Join(options.TranscodingTempPath, state.MediaSource.Id + ".concat");
+ _mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath);
+ arg.Append(" -f concat -safe 0 -i ")
+ .Append(tmpConcatPath);
+ }
+ else
+ {
+ arg.Append(" -i ")
+ .Append(GetInputPathArgument(state));
+ }
// sub2video for external graphical subtitles
if (state.SubtitleStream is not null
@@ -2074,14 +2116,20 @@ namespace MediaBrowser.Controller.MediaEncoding
private static double GetVideoBitrateScaleFactor(string codec)
{
+ // hevc & vp9 - 40% more efficient than h.264
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
{
return .6;
}
+ // av1 - 50% more efficient than h.264
+ if (string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase))
+ {
+ return .5;
+ }
+
return 1;
}
@@ -2089,7 +2137,9 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var inputScaleFactor = GetVideoBitrateScaleFactor(inputVideoCodec);
var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec);
- var scaleFactor = outputScaleFactor / inputScaleFactor;
+
+ // Don't scale the real bitrate lower than the requested bitrate
+ var scaleFactor = Math.Min(outputScaleFactor / inputScaleFactor, 1);
if (bitrate <= 500000)
{
@@ -2430,6 +2480,30 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
+ /// Gets the negative map args by filters.
+ /// </summary>
+ /// <param name="state">The state.</param>
+ /// <param name="videoProcessFilters">The videoProcessFilters.</param>
+ /// <returns>System.String.</returns>
+ public string GetNegativeMapArgsByFilters(EncodingJobInfo state, string videoProcessFilters)
+ {
+ string args = string.Empty;
+
+ // http://ffmpeg.org/ffmpeg-all.html#toc-Complex-filtergraphs-1
+ if (state.VideoStream != null && videoProcessFilters.Contains("-filter_complex", StringComparison.Ordinal))
+ {
+ int videoStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.VideoStream);
+
+ args += string.Format(
+ CultureInfo.InvariantCulture,
+ "-map -0:{0} ",
+ videoStreamIndex);
+ }
+
+ return args;
+ }
+
+ /// <summary>
/// Determines which stream will be used for playback.
/// </summary>
/// <param name="allStream">All stream.</param>
@@ -3271,7 +3345,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// OUTPUT nv12 surface(memory)
// prefer hwmap to hwdownload on opencl.
- var hwTransferFilter = hasGraphicalSubs ? "hwdownload" : "hwmap";
+ var hwTransferFilter = hasGraphicalSubs ? "hwdownload" : "hwmap=mode=read";
mainFilters.Add(hwTransferFilter);
mainFilters.Add("format=nv12");
}
@@ -3514,7 +3588,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// OUTPUT nv12 surface(memory)
// prefer hwmap to hwdownload on opencl.
// qsv hwmap is not fully implemented for the time being.
- mainFilters.Add(isHwmapUsable ? "hwmap" : "hwdownload");
+ mainFilters.Add(isHwmapUsable ? "hwmap=mode=read" : "hwdownload");
mainFilters.Add("format=nv12");
}
@@ -3672,6 +3746,13 @@ namespace MediaBrowser.Controller.MediaEncoding
var outFormat = doTonemap ? string.Empty : "nv12";
var hwScaleFilter = GetHwScaleFilter(isVaapiDecoder ? "vaapi" : "qsv", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+
+ // allocate extra pool sizes for vaapi vpp
+ if (!string.IsNullOrEmpty(hwScaleFilter) && isVaapiDecoder)
+ {
+ hwScaleFilter += ":extra_hw_frames=24";
+ }
+
// hw scale
mainFilters.Add(hwScaleFilter);
}
@@ -3718,7 +3799,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// OUTPUT nv12 surface(memory)
// prefer hwmap to hwdownload on opencl/vaapi.
// qsv hwmap is not fully implemented for the time being.
- mainFilters.Add(isHwmapUsable ? "hwmap" : "hwdownload");
+ mainFilters.Add(isHwmapUsable ? "hwmap=mode=read" : "hwdownload");
mainFilters.Add("format=nv12");
}
@@ -3947,6 +4028,13 @@ namespace MediaBrowser.Controller.MediaEncoding
var outFormat = doTonemap ? string.Empty : "nv12";
var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+
+ // allocate extra pool sizes for vaapi vpp
+ if (!string.IsNullOrEmpty(hwScaleFilter))
+ {
+ hwScaleFilter += ":extra_hw_frames=24";
+ }
+
// hw scale
mainFilters.Add(hwScaleFilter);
}
@@ -3988,7 +4076,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// OUTPUT nv12 surface(memory)
// prefer hwmap to hwdownload on opencl/vaapi.
- mainFilters.Add(isHwmapNotUsable ? "hwdownload" : "hwmap");
+ mainFilters.Add(isHwmapNotUsable ? "hwdownload" : "hwmap=mode=read");
mainFilters.Add("format=nv12");
}
@@ -4126,7 +4214,9 @@ namespace MediaBrowser.Controller.MediaEncoding
// sw => hw
if (doVkTonemap)
{
- mainFilters.Add("hwupload=derive_device=vulkan:extra_hw_frames=16");
+ mainFilters.Add("hwupload_vaapi");
+ mainFilters.Add("hwmap=derive_device=vulkan");
+ mainFilters.Add("format=vulkan");
}
}
else if (isVaapiDecoder)
@@ -4156,6 +4246,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// map from vaapi to vulkan via vaapi-vulkan interop (Vega/gfx9+).
mainFilters.Add("hwmap=derive_device=vulkan");
+ mainFilters.Add("format=vulkan");
}
// vk tonemap
@@ -4234,7 +4325,9 @@ namespace MediaBrowser.Controller.MediaEncoding
// prefer vaapi hwupload to vulkan hwupload,
// Mesa RADV does not support a dedicated transfer queue.
- subFilters.Add("hwupload=derive_device=vaapi,format=vaapi,hwmap=derive_device=vulkan");
+ subFilters.Add("hwupload_vaapi");
+ subFilters.Add("hwmap=derive_device=vulkan");
+ subFilters.Add("format=vulkan");
overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0");
overlayFilters.Add("scale_vulkan=format=nv12");
@@ -4336,6 +4429,13 @@ namespace MediaBrowser.Controller.MediaEncoding
outFormat = doOclTonemap ? string.Empty : "nv12";
var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+
+ // allocate extra pool sizes for vaapi vpp
+ if (!string.IsNullOrEmpty(hwScaleFilter))
+ {
+ hwScaleFilter += ":extra_hw_frames=24";
+ }
+
// hw scale
mainFilters.Add(hwScaleFilter);
}
@@ -4713,7 +4813,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// HWA decoders can handle both video files and video folders.
- var videoType = mediaSource.VideoType;
+ var videoType = state.VideoType;
if (videoType != VideoType.VideoFile
&& videoType != VideoType.Iso
&& videoType != VideoType.Dvd
@@ -4854,8 +4954,18 @@ namespace MediaBrowser.Controller.MediaEncoding
var isVideotoolboxSupported = isMacOS && _mediaEncoder.SupportsHwaccel("videotoolbox");
var isCodecAvailable = options.HardwareDecodingCodecs.Contains(videoCodec, StringComparison.OrdinalIgnoreCase);
+ var ffmpegVersion = _mediaEncoder.EncoderVersion;
+
// Set the av1 codec explicitly to trigger hw accelerator, otherwise libdav1d will be used.
- var isAv1 = string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase);
+ var isAv1 = ffmpegVersion < _minFFmpegImplictHwaccel
+ && string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase);
+
+ // Allow profile mismatch if decoding H.264 baseline with d3d11va and vaapi hwaccels.
+ var profileMismatch = string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(state.VideoStream?.Profile, "baseline", StringComparison.OrdinalIgnoreCase);
+
+ // Disable the extra internal copy in nvdec. We already handle it in filter chain.
+ var nvdecNoInternalCopy = ffmpegVersion >= _minFFmpegHwaUnsafeOutput;
if (bitDepth == 10 && isCodecAvailable)
{
@@ -4881,14 +4991,16 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (isVaapiSupported && isCodecAvailable)
{
- return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
+ return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty)
+ + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
}
if (isD3d11Supported && isCodecAvailable)
{
// set -threads 3 to intel d3d11va decoder explicitly. Lower threads may result in dead lock.
// on newer devices such as Xe, the larger the init_pool_size, the longer the initialization time for opencl to derive from d3d11.
- return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + " -threads 3" + (isAv1 ? " -c:v av1" : string.Empty);
+ return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty)
+ + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + " -threads 3" + (isAv1 ? " -c:v av1" : string.Empty);
}
}
else
@@ -4908,7 +5020,8 @@ namespace MediaBrowser.Controller.MediaEncoding
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);
+ return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty)
+ + (nvdecNoInternalCopy ? " -hwaccel_flags +unsafe_output" : string.Empty) + " -threads 1" + (isAv1 ? " -c:v av1" : string.Empty);
}
else
{
@@ -4923,7 +5036,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (isD3d11Supported && isCodecAvailable)
{
- return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
+ return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty)
+ + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
}
}
@@ -4932,9 +5046,11 @@ namespace MediaBrowser.Controller.MediaEncoding
&& isVaapiSupported
&& isCodecAvailable)
{
- return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
+ return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty)
+ + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
}
+ // Apple videotoolbox
if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)
&& isVideotoolboxSupported
&& isCodecAvailable)
@@ -5738,7 +5854,9 @@ namespace MediaBrowser.Controller.MediaEncoding
// video processing filters.
var videoProcessParam = GetVideoProcessingFilterParam(state, encodingOptions, videoCodec);
- args += videoProcessParam;
+ var negativeMapArgs = GetNegativeMapArgsByFilters(state, videoProcessParam);
+
+ args = negativeMapArgs + args + videoProcessParam;
hasCopyTs = videoProcessParam.Contains("copyts", StringComparison.OrdinalIgnoreCase);
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index bc6207ac5..f830b9f29 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -154,6 +154,14 @@ namespace MediaBrowser.Controller.MediaEncoding
string GetInputArgument(string inputFile, MediaSourceInfo mediaSource);
/// <summary>
+ /// Gets the input argument.
+ /// </summary>
+ /// <param name="inputFiles">The input files.</param>
+ /// <param name="mediaSource">The mediaSource.</param>
+ /// <returns>System.String.</returns>
+ string GetInputArgument(IReadOnlyList<string> inputFiles, MediaSourceInfo mediaSource);
+
+ /// <summary>
/// Gets the input argument for an external subtitle file.
/// </summary>
/// <param name="inputFile">The input file.</param>
@@ -187,5 +195,27 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <param name="path">The path.</param>
/// <param name="pathType">The type of path.</param>
void UpdateEncoderPath(string path, string pathType);
+
+ /// <summary>
+ /// Gets the primary playlist of .vob files.
+ /// </summary>
+ /// <param name="path">The to the .vob files.</param>
+ /// <param name="titleNumber">The title number to start with.</param>
+ /// <returns>A playlist.</returns>
+ IReadOnlyList<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber);
+
+ /// <summary>
+ /// Gets the primary playlist of .m2ts files.
+ /// </summary>
+ /// <param name="path">The to the .m2ts files.</param>
+ /// <returns>A playlist.</returns>
+ IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path);
+
+ /// <summary>
+ /// Generates a FFmpeg concat config for the source.
+ /// </summary>
+ /// <param name="source">The <see cref="MediaSourceInfo"/>.</param>
+ /// <param name="concatFilePath">The path the config should be written to.</param>
+ void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath);
}
}
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index db177ff76..80091bf5a 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -14,6 +14,7 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.MediaEncoding.Encoder;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
@@ -301,10 +302,10 @@ namespace MediaBrowser.MediaEncoding.Attachments
var processArgs = string.Format(
CultureInfo.InvariantCulture,
- "-dump_attachment:{1} {2} -i {0} -t 0 -f null null",
+ "-dump_attachment:{1} \"{2}\" -i {0} -t 0 -f null null",
inputPath,
attachmentStreamIndex,
- outputPath);
+ EncodingUtils.NormalizePath(outputPath));
int exitCode;
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
new file mode 100644
index 000000000..ea520b1d6
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
@@ -0,0 +1,123 @@
+using System;
+using System.IO;
+using System.Linq;
+using BDInfo.IO;
+using MediaBrowser.Model.IO;
+
+namespace MediaBrowser.MediaEncoding.BdInfo;
+
+/// <summary>
+/// Class BdInfoDirectoryInfo.
+/// </summary>
+public class BdInfoDirectoryInfo : IDirectoryInfo
+{
+ private readonly IFileSystem _fileSystem;
+
+ private readonly FileSystemMetadata _impl;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BdInfoDirectoryInfo" /> class.
+ /// </summary>
+ /// <param name="fileSystem">The filesystem.</param>
+ /// <param name="path">The path.</param>
+ public BdInfoDirectoryInfo(IFileSystem fileSystem, string path)
+ {
+ _fileSystem = fileSystem;
+ _impl = _fileSystem.GetDirectoryInfo(path);
+ }
+
+ private BdInfoDirectoryInfo(IFileSystem fileSystem, FileSystemMetadata impl)
+ {
+ _fileSystem = fileSystem;
+ _impl = impl;
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ public string Name => _impl.Name;
+
+ /// <summary>
+ /// Gets the full name.
+ /// </summary>
+ public string FullName => _impl.FullName;
+
+ /// <summary>
+ /// Gets the parent directory information.
+ /// </summary>
+ public IDirectoryInfo? Parent
+ {
+ get
+ {
+ var parentFolder = Path.GetDirectoryName(_impl.FullName);
+ if (parentFolder is not null)
+ {
+ return new BdInfoDirectoryInfo(_fileSystem, parentFolder);
+ }
+
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Gets the directories.
+ /// </summary>
+ /// <returns>An array with all directories.</returns>
+ public IDirectoryInfo[] GetDirectories()
+ {
+ return _fileSystem.GetDirectories(_impl.FullName)
+ .Select(x => new BdInfoDirectoryInfo(_fileSystem, x))
+ .ToArray();
+ }
+
+ /// <summary>
+ /// Gets the files.
+ /// </summary>
+ /// <returns>All files of the directory.</returns>
+ public IFileInfo[] GetFiles()
+ {
+ return _fileSystem.GetFiles(_impl.FullName)
+ .Select(x => new BdInfoFileInfo(x))
+ .ToArray();
+ }
+
+ /// <summary>
+ /// Gets the files matching a pattern.
+ /// </summary>
+ /// <param name="searchPattern">The search pattern.</param>
+ /// <returns>All files of the directory matchign the search pattern.</returns>
+ public IFileInfo[] GetFiles(string searchPattern)
+ {
+ return _fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false)
+ .Select(x => new BdInfoFileInfo(x))
+ .ToArray();
+ }
+
+ /// <summary>
+ /// Gets the files matching a pattern and search options.
+ /// </summary>
+ /// <param name="searchPattern">The search pattern.</param>
+ /// <param name="searchOption">The search optin.</param>
+ /// <returns>All files of the directory matchign the search pattern and options.</returns>
+ public IFileInfo[] GetFiles(string searchPattern, SearchOption searchOption)
+ {
+ return _fileSystem.GetFiles(
+ _impl.FullName,
+ new[] { searchPattern },
+ false,
+ (searchOption & SearchOption.AllDirectories) == SearchOption.AllDirectories)
+ .Select(x => new BdInfoFileInfo(x))
+ .ToArray();
+ }
+
+ /// <summary>
+ /// Gets the bdinfo of a file system path.
+ /// </summary>
+ /// <param name="fs">The file system.</param>
+ /// <param name="path">The path.</param>
+ /// <returns>The BD directory information of the path on the file system.</returns>
+ public static IDirectoryInfo FromFileSystemPath(IFileSystem fs, string path)
+ {
+ return new BdInfoDirectoryInfo(fs, path);
+ }
+}
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs
new file mode 100644
index 000000000..8ebb59c59
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs
@@ -0,0 +1,187 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using BDInfo;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+
+namespace MediaBrowser.MediaEncoding.BdInfo;
+
+/// <summary>
+/// Class BdInfoExaminer.
+/// </summary>
+public class BdInfoExaminer : IBlurayExaminer
+{
+ private readonly IFileSystem _fileSystem;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BdInfoExaminer" /> class.
+ /// </summary>
+ /// <param name="fileSystem">The filesystem.</param>
+ public BdInfoExaminer(IFileSystem fileSystem)
+ {
+ _fileSystem = fileSystem;
+ }
+
+ /// <summary>
+ /// Gets the disc info.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>BlurayDiscInfo.</returns>
+ public BlurayDiscInfo GetDiscInfo(string path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ throw new ArgumentNullException(nameof(path));
+ }
+
+ var bdrom = new BDROM(BdInfoDirectoryInfo.FromFileSystemPath(_fileSystem, path));
+
+ bdrom.Scan();
+
+ // Get the longest playlist
+ var playlist = bdrom.PlaylistFiles.Values.OrderByDescending(p => p.TotalLength).FirstOrDefault(p => p.IsValid);
+
+ var outputStream = new BlurayDiscInfo
+ {
+ MediaStreams = Array.Empty<MediaStream>()
+ };
+
+ if (playlist is null)
+ {
+ return outputStream;
+ }
+
+ outputStream.Chapters = playlist.Chapters.ToArray();
+
+ outputStream.RunTimeTicks = TimeSpan.FromSeconds(playlist.TotalLength).Ticks;
+
+ var sortedStreams = playlist.SortedStreams;
+ var mediaStreams = new List<MediaStream>(sortedStreams.Count);
+
+ foreach (var stream in sortedStreams)
+ {
+ switch (stream)
+ {
+ case TSVideoStream videoStream:
+ AddVideoStream(mediaStreams, videoStream);
+ break;
+ case TSAudioStream audioStream:
+ AddAudioStream(mediaStreams, audioStream);
+ break;
+ case TSTextStream textStream:
+ AddSubtitleStream(mediaStreams, textStream);
+ break;
+ case TSGraphicsStream graphicStream:
+ AddSubtitleStream(mediaStreams, graphicStream);
+ break;
+ }
+ }
+
+ outputStream.MediaStreams = mediaStreams.ToArray();
+
+ outputStream.PlaylistName = playlist.Name;
+
+ if (playlist.StreamClips is not null && playlist.StreamClips.Count > 0)
+ {
+ // Get the files in the playlist
+ outputStream.Files = playlist.StreamClips.Select(i => i.StreamFile.Name).ToArray();
+ }
+
+ return outputStream;
+ }
+
+ /// <summary>
+ /// Adds the video stream.
+ /// </summary>
+ /// <param name="streams">The streams.</param>
+ /// <param name="videoStream">The video stream.</param>
+ private void AddVideoStream(List<MediaStream> streams, TSVideoStream videoStream)
+ {
+ var mediaStream = new MediaStream
+ {
+ BitRate = Convert.ToInt32(videoStream.BitRate),
+ Width = videoStream.Width,
+ Height = videoStream.Height,
+ Codec = videoStream.CodecShortName,
+ IsInterlaced = videoStream.IsInterlaced,
+ Type = MediaStreamType.Video,
+ Index = streams.Count
+ };
+
+ if (videoStream.FrameRateDenominator > 0)
+ {
+ float frameRateEnumerator = videoStream.FrameRateEnumerator;
+ float frameRateDenominator = videoStream.FrameRateDenominator;
+
+ mediaStream.AverageFrameRate = mediaStream.RealFrameRate = frameRateEnumerator / frameRateDenominator;
+ }
+
+ streams.Add(mediaStream);
+ }
+
+ /// <summary>
+ /// Adds the audio stream.
+ /// </summary>
+ /// <param name="streams">The streams.</param>
+ /// <param name="audioStream">The audio stream.</param>
+ private void AddAudioStream(List<MediaStream> streams, TSAudioStream audioStream)
+ {
+ var stream = new MediaStream
+ {
+ Codec = audioStream.CodecShortName,
+ Language = audioStream.LanguageCode,
+ Channels = audioStream.ChannelCount,
+ SampleRate = audioStream.SampleRate,
+ Type = MediaStreamType.Audio,
+ Index = streams.Count
+ };
+
+ var bitrate = Convert.ToInt32(audioStream.BitRate);
+
+ if (bitrate > 0)
+ {
+ stream.BitRate = bitrate;
+ }
+
+ if (audioStream.LFE > 0)
+ {
+ stream.Channels = audioStream.ChannelCount + 1;
+ }
+
+ streams.Add(stream);
+ }
+
+ /// <summary>
+ /// Adds the subtitle stream.
+ /// </summary>
+ /// <param name="streams">The streams.</param>
+ /// <param name="textStream">The text stream.</param>
+ private void AddSubtitleStream(List<MediaStream> streams, TSTextStream textStream)
+ {
+ streams.Add(new MediaStream
+ {
+ Language = textStream.LanguageCode,
+ Codec = textStream.CodecShortName,
+ Type = MediaStreamType.Subtitle,
+ Index = streams.Count
+ });
+ }
+
+ /// <summary>
+ /// Adds the subtitle stream.
+ /// </summary>
+ /// <param name="streams">The streams.</param>
+ /// <param name="textStream">The text stream.</param>
+ private void AddSubtitleStream(List<MediaStream> streams, TSGraphicsStream textStream)
+ {
+ streams.Add(new MediaStream
+ {
+ Language = textStream.LanguageCode,
+ Codec = textStream.CodecShortName,
+ Type = MediaStreamType.Subtitle,
+ Index = streams.Count
+ });
+ }
+}
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
new file mode 100644
index 000000000..9e7a1d50a
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
@@ -0,0 +1,68 @@
+using System.IO;
+using MediaBrowser.Model.IO;
+
+namespace MediaBrowser.MediaEncoding.BdInfo;
+
+/// <summary>
+/// Class BdInfoFileInfo.
+/// </summary>
+public class BdInfoFileInfo : BDInfo.IO.IFileInfo
+{
+ private FileSystemMetadata _impl;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BdInfoFileInfo" /> class.
+ /// </summary>
+ /// <param name="impl">The <see cref="FileSystemMetadata" />.</param>
+ public BdInfoFileInfo(FileSystemMetadata impl)
+ {
+ _impl = impl;
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ public string Name => _impl.Name;
+
+ /// <summary>
+ /// Gets the full name.
+ /// </summary>
+ public string FullName => _impl.FullName;
+
+ /// <summary>
+ /// Gets the extension.
+ /// </summary>
+ public string Extension => _impl.Extension;
+
+ /// <summary>
+ /// Gets the length.
+ /// </summary>
+ public long Length => _impl.Length;
+
+ /// <summary>
+ /// Gets a value indicating whether this is a directory.
+ /// </summary>
+ public bool IsDir => _impl.IsDirectory;
+
+ /// <summary>
+ /// Gets a file as file stream.
+ /// </summary>
+ /// <returns>A <see cref="FileStream" /> for the file.</returns>
+ public Stream OpenRead()
+ {
+ return new FileStream(
+ FullName,
+ FileMode.Open,
+ FileAccess.Read,
+ FileShare.Read);
+ }
+
+ /// <summary>
+ /// Gets a files's content with a stream reader.
+ /// </summary>
+ /// <returns>A <see cref="StreamReader" /> for the file's content.</returns>
+ public StreamReader OpenText()
+ {
+ return new StreamReader(OpenRead());
+ }
+}
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
index d0ea0429b..04128c911 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
@@ -1,7 +1,9 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Generic;
using System.Globalization;
+using System.Linq;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Encoder
@@ -15,21 +17,38 @@ namespace MediaBrowser.MediaEncoding.Encoder
return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", inputFile);
}
- return GetConcatInputArgument(inputFile, inputPrefix);
+ return GetFileInputArgument(inputFile, inputPrefix);
+ }
+
+ public static string GetInputArgument(string inputPrefix, IReadOnlyList<string> inputFiles, MediaProtocol protocol)
+ {
+ if (protocol != MediaProtocol.File)
+ {
+ return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", inputFiles[0]);
+ }
+
+ return GetConcatInputArgument(inputFiles, inputPrefix);
}
/// <summary>
/// Gets the concat input argument.
/// </summary>
- /// <param name="inputFile">The input file.</param>
+ /// <param name="inputFiles">The input files.</param>
/// <param name="inputPrefix">The input prefix.</param>
/// <returns>System.String.</returns>
- private static string GetConcatInputArgument(string inputFile, string inputPrefix)
+ private static string GetConcatInputArgument(IReadOnlyList<string> inputFiles, string inputPrefix)
{
// Get all streams
// If there's more than one we'll need to use the concat command
+ if (inputFiles.Count > 1)
+ {
+ var files = string.Join("|", inputFiles.Select(NormalizePath));
+
+ return string.Format(CultureInfo.InvariantCulture, "concat:\"{0}\"", files);
+ }
+
// Determine the input path for video files
- return GetFileInputArgument(inputFile, inputPrefix);
+ return GetFileInputArgument(inputFiles[0], inputPrefix);
}
/// <summary>
@@ -56,7 +75,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// </summary>
/// <param name="path">The path.</param>
/// <returns>System.String.</returns>
- private static string NormalizePath(string path)
+ public static string NormalizePath(string path)
{
// Quotes are valid path characters in linux and they need to be escaped here with a leading \
return path.Replace("\"", "\\\"", StringComparison.Ordinal);
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index cef02d5f8..d2112e5dc 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -11,6 +11,7 @@ using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Common;
@@ -51,6 +52,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private readonly IServerConfigurationManager _configurationManager;
private readonly IFileSystem _fileSystem;
private readonly ILocalizationManager _localization;
+ private readonly IBlurayExaminer _blurayExaminer;
private readonly IConfiguration _config;
private readonly IServerConfigurationManager _serverConfig;
private readonly string _startupOptionFFmpegPath;
@@ -95,6 +97,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
ILogger<MediaEncoder> logger,
IServerConfigurationManager configurationManager,
IFileSystem fileSystem,
+ IBlurayExaminer blurayExaminer,
ILocalizationManager localization,
IConfiguration config,
IServerConfigurationManager serverConfig)
@@ -102,6 +105,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_logger = logger;
_configurationManager = configurationManager;
_fileSystem = fileSystem;
+ _blurayExaminer = blurayExaminer;
_localization = localization;
_config = config;
_serverConfig = serverConfig;
@@ -117,16 +121,22 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <inheritdoc />
public string ProbePath => _ffprobePath;
+ /// <inheritdoc />
public Version EncoderVersion => _ffmpegVersion;
+ /// <inheritdoc />
public bool IsPkeyPauseSupported => _isPkeyPauseSupported;
+ /// <inheritdoc />
public bool IsVaapiDeviceAmd => _isVaapiDeviceAmd;
+ /// <inheritdoc />
public bool IsVaapiDeviceInteliHD => _isVaapiDeviceInteliHD;
+ /// <inheritdoc />
public bool IsVaapiDeviceInteli965 => _isVaapiDeviceInteli965;
+ /// <inheritdoc />
public bool IsVaapiDeviceSupportVulkanFmtModifier => _isVaapiDeviceSupportVulkanFmtModifier;
/// <summary>
@@ -344,26 +354,31 @@ namespace MediaBrowser.MediaEncoding.Encoder
_ffmpegVersion = validator.GetFFmpegVersion();
}
+ /// <inheritdoc />
public bool SupportsEncoder(string encoder)
{
return _encoders.Contains(encoder, StringComparer.OrdinalIgnoreCase);
}
+ /// <inheritdoc />
public bool SupportsDecoder(string decoder)
{
return _decoders.Contains(decoder, StringComparer.OrdinalIgnoreCase);
}
+ /// <inheritdoc />
public bool SupportsHwaccel(string hwaccel)
{
return _hwaccels.Contains(hwaccel, StringComparer.OrdinalIgnoreCase);
}
+ /// <inheritdoc />
public bool SupportsFilter(string filter)
{
return _filters.Contains(filter, StringComparer.OrdinalIgnoreCase);
}
+ /// <inheritdoc />
public bool SupportsFilterWithOption(FilterOptionType option)
{
if (_filtersWithOption.TryGetValue((int)option, out var val))
@@ -394,24 +409,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
return true;
}
- /// <summary>
- /// Gets the media info.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
+ /// <inheritdoc />
public Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken)
{
var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters;
- var inputFile = request.MediaSource.Path;
-
string analyzeDuration = string.Empty;
string ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
if (request.MediaSource.AnalyzeDurationMs > 0)
{
- analyzeDuration = "-analyzeduration " +
- (request.MediaSource.AnalyzeDurationMs * 1000).ToString();
+ analyzeDuration = "-analyzeduration " + (request.MediaSource.AnalyzeDurationMs * 1000).ToString();
}
else if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
{
@@ -419,7 +426,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
return GetMediaInfoInternal(
- GetInputArgument(inputFile, request.MediaSource),
+ GetInputArgument(request.MediaSource.Path, request.MediaSource),
request.MediaSource.Path,
request.MediaSource.Protocol,
extractChapters,
@@ -429,36 +436,30 @@ namespace MediaBrowser.MediaEncoding.Encoder
cancellationToken);
}
- /// <summary>
- /// Gets the input argument.
- /// </summary>
- /// <param name="inputFile">The input file.</param>
- /// <param name="mediaSource">The mediaSource.</param>
- /// <returns>System.String.</returns>
- /// <exception cref="ArgumentException">Unrecognized InputType.</exception>
+ /// <inheritdoc />
+ public string GetInputArgument(IReadOnlyList<string> inputFiles, MediaSourceInfo mediaSource)
+ {
+ return EncodingUtils.GetInputArgument("file", inputFiles, mediaSource.Protocol);
+ }
+
+ /// <inheritdoc />
public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource)
{
var prefix = "file";
- if (mediaSource.VideoType == VideoType.BluRay
- || mediaSource.IsoType == IsoType.BluRay)
+ if (mediaSource.IsoType == IsoType.BluRay)
{
prefix = "bluray";
}
- return EncodingUtils.GetInputArgument(prefix, inputFile, mediaSource.Protocol);
+ return EncodingUtils.GetInputArgument(prefix, new[] { inputFile }, mediaSource.Protocol);
}
- /// <summary>
- /// Gets the input argument for an external subtitle file.
- /// </summary>
- /// <param name="inputFile">The input file.</param>
- /// <returns>System.String.</returns>
- /// <exception cref="ArgumentException">Unrecognized InputType.</exception>
+ /// <inheritdoc />
public string GetExternalSubtitleInputArgument(string inputFile)
{
const string Prefix = "file";
- return EncodingUtils.GetInputArgument(Prefix, inputFile, MediaProtocol.File);
+ return EncodingUtils.GetInputArgument(Prefix, new[] { inputFile }, MediaProtocol.File);
}
/// <summary>
@@ -549,6 +550,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
+ /// <inheritdoc />
public Task<string> ExtractAudioImage(string path, int? imageStreamIndex, CancellationToken cancellationToken)
{
var mediaSource = new MediaSourceInfo
@@ -559,11 +561,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, ImageFormat.Jpg, cancellationToken);
}
+ /// <inheritdoc />
public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken)
{
return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, ImageFormat.Jpg, cancellationToken);
}
+ /// <inheritdoc />
public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, ImageFormat? targetFormat, CancellationToken cancellationToken)
{
return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, targetFormat, cancellationToken);
@@ -767,6 +771,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
+ /// <inheritdoc />
public string GetTimeParameter(long ticks)
{
var time = TimeSpan.FromTicks(ticks);
@@ -865,6 +870,114 @@ namespace MediaBrowser.MediaEncoding.Encoder
throw new NotImplementedException();
}
+ /// <inheritdoc />
+ public IReadOnlyList<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber)
+ {
+ // Eliminate menus and intros by omitting VIDEO_TS.VOB and all subsequent title .vob files ending with _0.VOB
+ var allVobs = _fileSystem.GetFiles(path, true)
+ .Where(file => string.Equals(file.Extension, ".VOB", StringComparison.OrdinalIgnoreCase))
+ .Where(file => !string.Equals(file.Name, "VIDEO_TS.VOB", StringComparison.OrdinalIgnoreCase))
+ .Where(file => !file.Name.EndsWith("_0.VOB", StringComparison.OrdinalIgnoreCase))
+ .OrderBy(i => i.FullName)
+ .ToList();
+
+ if (titleNumber.HasValue)
+ {
+ var prefix = string.Format(CultureInfo.InvariantCulture, "VTS_{0:D2}_", titleNumber.Value);
+ var vobs = allVobs.Where(i => i.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList();
+
+ if (vobs.Count > 0)
+ {
+ return vobs.Select(i => i.FullName).ToList();
+ }
+
+ _logger.LogWarning("Could not determine .vob files for title {Title} of {Path}.", titleNumber, path);
+ }
+
+ // Check for multiple big titles (> 900 MB)
+ var titles = allVobs
+ .Where(vob => vob.Length >= 900 * 1024 * 1024)
+ .Select(vob => _fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToString())
+ .Distinct()
+ .ToList();
+
+ // Fall back to first title if no big title is found
+ if (titles.Count == 0)
+ {
+ titles.Add(_fileSystem.GetFileNameWithoutExtension(allVobs[0]).AsSpan().RightPart('_').ToString());
+ }
+
+ // Aggregate all .vob files of the titles
+ return allVobs
+ .Where(vob => titles.Contains(_fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToString()))
+ .Select(i => i.FullName)
+ .ToList();
+ }
+
+ /// <inheritdoc />
+ public IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path)
+ {
+ // Get all playable .m2ts files
+ var validPlaybackFiles = _blurayExaminer.GetDiscInfo(path).Files;
+
+ // Get all files from the BDMV/STREAMING directory
+ var directoryFiles = _fileSystem.GetFiles(Path.Join(path, "BDMV", "STREAM"));
+
+ // Only return playable local .m2ts files
+ return directoryFiles
+ .Where(f => validPlaybackFiles.Contains(f.Name, StringComparer.OrdinalIgnoreCase))
+ .Select(f => f.FullName)
+ .ToList();
+ }
+
+ /// <inheritdoc />
+ public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath)
+ {
+ // Get all playable files
+ IReadOnlyList<string> files;
+ var videoType = source.VideoType;
+ if (videoType == VideoType.Dvd)
+ {
+ files = GetPrimaryPlaylistVobFiles(source.Path, null);
+ }
+ else if (videoType == VideoType.BluRay)
+ {
+ files = GetPrimaryPlaylistM2tsFiles(source.Path);
+ }
+ else
+ {
+ return;
+ }
+
+ // Generate concat configuration entries for each file and write to file
+ using (StreamWriter sw = new StreamWriter(concatFilePath))
+ {
+ foreach (var path in files)
+ {
+ var mediaInfoResult = GetMediaInfo(
+ new MediaInfoRequest
+ {
+ MediaType = DlnaProfileType.Video,
+ MediaSource = new MediaSourceInfo
+ {
+ Path = path,
+ Protocol = MediaProtocol.File,
+ VideoType = videoType
+ }
+ },
+ CancellationToken.None).GetAwaiter().GetResult();
+
+ var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds;
+
+ // Add file path stanza to concat configuration
+ sw.WriteLine("file '{0}'", path);
+
+ // Add duration stanza to concat configuration
+ sw.WriteLine("duration {0}", duration);
+ }
+ }
+ }
+
public bool CanExtractSubtitles(string codec)
{
// TODO is there ever a case when a subtitle can't be extracted??
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index f4438fe19..a0624fe76 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -22,6 +22,7 @@
</ItemGroup>
<ItemGroup>
+ <PackageReference Include="BDInfo" />
<PackageReference Include="libse" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="System.Text.Encoding.CodePages" />
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 8b8279588..cb482301f 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -248,12 +248,23 @@ namespace MediaBrowser.MediaEncoding.Probing
return null;
}
+ // Handle MPEG-1 container
if (string.Equals(format, "mpegvideo", StringComparison.OrdinalIgnoreCase))
{
return "mpeg";
}
- format = format.Replace("matroska", "mkv", StringComparison.OrdinalIgnoreCase);
+ // Handle MPEG-2 container
+ if (string.Equals(format, "mpeg", StringComparison.OrdinalIgnoreCase))
+ {
+ return "ts";
+ }
+
+ // Handle matroska container
+ if (string.Equals(format, "matroska", StringComparison.OrdinalIgnoreCase))
+ {
+ return "mkv";
+ }
return format;
}
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index 0ff95a2e1..f348d8417 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -39,7 +39,8 @@ public class EncodingOptions
DeinterlaceMethod = "yadif";
EnableDecodingColorDepth10Hevc = true;
EnableDecodingColorDepth10Vp9 = true;
- EnableEnhancedNvdecDecoder = false;
+ // Enhanced Nvdec or system native decoder is required for DoVi to SDR tone-mapping.
+ EnableEnhancedNvdecDecoder = true;
PreferSystemNativeHwDecoder = true;
EnableIntelLowPowerH264HwEncoder = false;
EnableIntelLowPowerHevcHwEncoder = false;
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index c39162250..7cb07a2f7 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -243,16 +243,10 @@ namespace MediaBrowser.Model.Configuration
public bool AllowClientLogUpload { get; set; } = true;
/// <summary>
- /// Gets or sets the dummy chapters duration in seconds.
+ /// Gets or sets the dummy chapter duration in seconds, use 0 (zero) or less to disable generation alltogether.
/// </summary>
/// <value>The dummy chapters duration.</value>
- public int DummyChapterDuration { get; set; } = 300;
-
- /// <summary>
- /// Gets or sets the dummy chapter count.
- /// </summary>
- /// <value>The dummy chapter count.</value>
- public int DummyChapterCount { get; set; } = 100;
+ public int DummyChapterDuration { get; set; } = 0;
/// <summary>
/// Gets or sets the chapter image resolution.
diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
index 00b406bbe..f5e1a3c49 100644
--- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs
+++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
@@ -136,12 +136,26 @@ namespace MediaBrowser.Model.Dlna
return !condition.IsRequired;
}
- if (int.TryParse(condition.Value, CultureInfo.InvariantCulture, out var expected))
+ var conditionType = condition.Condition;
+ if (condition.Condition == ProfileConditionType.EqualsAny)
{
- switch (condition.Condition)
+ foreach (var singleConditionString in condition.Value.AsSpan().Split('|'))
+ {
+ if (int.TryParse(singleConditionString, NumberStyles.Integer, CultureInfo.InvariantCulture, out int conditionValue)
+ && conditionValue.Equals(currentValue))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ if (int.TryParse(condition.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var expected))
+ {
+ switch (conditionType)
{
case ProfileConditionType.Equals:
- case ProfileConditionType.EqualsAny:
return currentValue.Value.Equals(expected);
case ProfileConditionType.GreaterThanEqual:
return currentValue.Value >= expected;
@@ -212,9 +226,24 @@ namespace MediaBrowser.Model.Dlna
return !condition.IsRequired;
}
- if (double.TryParse(condition.Value, CultureInfo.InvariantCulture, out var expected))
+ var conditionType = condition.Condition;
+ if (condition.Condition == ProfileConditionType.EqualsAny)
{
- switch (condition.Condition)
+ foreach (var singleConditionString in condition.Value.AsSpan().Split('|'))
+ {
+ if (double.TryParse(singleConditionString, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out double conditionValue)
+ && conditionValue.Equals(currentValue))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ if (double.TryParse(condition.Value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var expected))
+ {
+ switch (conditionType)
{
case ProfileConditionType.Equals:
return currentValue.Value.Equals(expected);
diff --git a/MediaBrowser.Model/Dlna/ContainerProfile.cs b/MediaBrowser.Model/Dlna/ContainerProfile.cs
index 927df8e4e..978004268 100644
--- a/MediaBrowser.Model/Dlna/ContainerProfile.cs
+++ b/MediaBrowser.Model/Dlna/ContainerProfile.cs
@@ -11,7 +11,7 @@ namespace MediaBrowser.Model.Dlna
[XmlAttribute("type")]
public DlnaProfileType Type { get; set; }
- public ProfileCondition[]? Conditions { get; set; } = Array.Empty<ProfileCondition>();
+ public ProfileCondition[] Conditions { get; set; } = Array.Empty<ProfileCondition>();
[XmlAttribute("container")]
public string Container { get; set; } = string.Empty;
diff --git a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
index 03c3a7265..f68235d86 100644
--- a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
+++ b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
@@ -18,17 +18,17 @@ namespace MediaBrowser.Model.Dlna
[XmlAttribute("type")]
public DlnaProfileType Type { get; set; }
- public bool SupportsContainer(string container)
+ public bool SupportsContainer(string? container)
{
return ContainerProfile.ContainsContainer(Container, container);
}
- public bool SupportsVideoCodec(string codec)
+ public bool SupportsVideoCodec(string? codec)
{
return Type == DlnaProfileType.Video && ContainerProfile.ContainsContainer(VideoCodec, codec);
}
- public bool SupportsAudioCodec(string codec)
+ public bool SupportsAudioCodec(string? codec)
{
return (Type == DlnaProfileType.Audio || Type == DlnaProfileType.Video) && ContainerProfile.ContainsContainer(AudioCodec, codec);
}
diff --git a/MediaBrowser.Model/Dlna/ITranscoderSupport.cs b/MediaBrowser.Model/Dlna/ITranscoderSupport.cs
index a70ce44cc..d7397399d 100644
--- a/MediaBrowser.Model/Dlna/ITranscoderSupport.cs
+++ b/MediaBrowser.Model/Dlna/ITranscoderSupport.cs
@@ -10,22 +10,4 @@ namespace MediaBrowser.Model.Dlna
bool CanExtractSubtitles(string codec);
}
-
- public class FullTranscoderSupport : ITranscoderSupport
- {
- public bool CanEncodeToAudioCodec(string codec)
- {
- return true;
- }
-
- public bool CanEncodeToSubtitleCodec(string codec)
- {
- return true;
- }
-
- public bool CanExtractSubtitles(string codec)
- {
- return true;
- }
- }
}
diff --git a/MediaBrowser.Model/Dlna/MediaOptions.cs b/MediaBrowser.Model/Dlna/MediaOptions.cs
index 29aecf97f..7ec0dd473 100644
--- a/MediaBrowser.Model/Dlna/MediaOptions.cs
+++ b/MediaBrowser.Model/Dlna/MediaOptions.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using MediaBrowser.Model.Dto;
@@ -59,22 +57,22 @@ namespace MediaBrowser.Model.Dlna
/// <summary>
/// Gets or sets the media sources.
/// </summary>
- public MediaSourceInfo[] MediaSources { get; set; }
+ public MediaSourceInfo[] MediaSources { get; set; } = Array.Empty<MediaSourceInfo>();
/// <summary>
/// Gets or sets the device profile.
/// </summary>
- public DeviceProfile Profile { get; set; }
+ required public DeviceProfile Profile { get; set; }
/// <summary>
/// Gets or sets a media source id. Optional. Only needed if a specific AudioStreamIndex or SubtitleStreamIndex are requested.
/// </summary>
- public string MediaSourceId { get; set; }
+ public string? MediaSourceId { get; set; }
/// <summary>
/// Gets or sets the device id.
/// </summary>
- public string DeviceId { get; set; }
+ public string? DeviceId { get; set; }
/// <summary>
/// Gets or sets an override of supported number of audio channels
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index ab81bfb34..84c8c012c 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -38,53 +36,36 @@ namespace MediaBrowser.Model.Dlna
}
/// <summary>
- /// Initializes a new instance of the <see cref="StreamBuilder"/> class.
- /// </summary>
- /// <param name="logger">The <see cref="ILogger"/> object.</param>
- public StreamBuilder(ILogger<StreamBuilder> logger)
- : this(new FullTranscoderSupport(), logger)
- {
- }
-
- /// <summary>
/// Gets the optimal audio stream.
/// </summary>
/// <param name="options">The <see cref="MediaOptions"/> object to get the audio stream from.</param>
/// <returns>The <see cref="StreamInfo"/> of the optimal audio stream.</returns>
- public StreamInfo GetOptimalAudioStream(MediaOptions options)
+ public StreamInfo? GetOptimalAudioStream(MediaOptions options)
{
ValidateMediaOptions(options, false);
- var mediaSources = new List<MediaSourceInfo>();
+ var streams = new List<StreamInfo>();
foreach (var mediaSource in options.MediaSources)
{
- if (string.IsNullOrEmpty(options.MediaSourceId) ||
- string.Equals(mediaSource.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase))
+ if (!(string.IsNullOrEmpty(options.MediaSourceId)
+ || string.Equals(mediaSource.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)))
{
- mediaSources.Add(mediaSource);
+ continue;
}
- }
- var streams = new List<StreamInfo>();
- foreach (var mediaSourceInfo in mediaSources)
- {
- StreamInfo streamInfo = GetOptimalAudioStream(mediaSourceInfo, options);
+ StreamInfo? streamInfo = GetOptimalAudioStream(mediaSource, options);
if (streamInfo is not null)
{
+ streamInfo.DeviceId = options.DeviceId;
+ streamInfo.DeviceProfileId = options.Profile.Id;
streams.Add(streamInfo);
}
}
- foreach (var stream in streams)
- {
- stream.DeviceId = options.DeviceId;
- stream.DeviceProfileId = options.Profile.Id;
- }
-
return GetOptimalStream(streams, options.GetMaxBitrate(true) ?? 0);
}
- private StreamInfo GetOptimalAudioStream(MediaSourceInfo item, MediaOptions options)
+ private StreamInfo? GetOptimalAudioStream(MediaSourceInfo item, MediaOptions options)
{
var playlistItem = new StreamInfo
{
@@ -138,7 +119,7 @@ namespace MediaBrowser.Model.Dlna
}
}
- TranscodingProfile transcodingProfile = null;
+ TranscodingProfile? transcodingProfile = null;
foreach (var tcProfile in options.Profile.TranscodingProfiles)
{
if (tcProfile.Type == playlistItem.MediaType
@@ -190,15 +171,15 @@ namespace MediaBrowser.Model.Dlna
/// </summary>
/// <param name="options">The <see cref="MediaOptions"/> object to get the video stream from.</param>
/// <returns>The <see cref="StreamInfo"/> of the optimal video stream.</returns>
- public StreamInfo GetOptimalVideoStream(MediaOptions options)
+ public StreamInfo? GetOptimalVideoStream(MediaOptions options)
{
ValidateMediaOptions(options, true);
var mediaSources = new List<MediaSourceInfo>();
foreach (var mediaSourceInfo in options.MediaSources)
{
- if (string.IsNullOrEmpty(options.MediaSourceId) ||
- string.Equals(mediaSourceInfo.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase))
+ if (string.IsNullOrEmpty(options.MediaSourceId)
+ || string.Equals(mediaSourceInfo.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase))
{
mediaSources.Add(mediaSourceInfo);
}
@@ -223,7 +204,7 @@ namespace MediaBrowser.Model.Dlna
return GetOptimalStream(streams, options.GetMaxBitrate(false) ?? 0);
}
- private static StreamInfo GetOptimalStream(List<StreamInfo> streams, long maxBitrate)
+ private static StreamInfo? GetOptimalStream(List<StreamInfo> streams, long maxBitrate)
=> SortMediaSources(streams, maxBitrate).FirstOrDefault();
private static IOrderedEnumerable<StreamInfo> SortMediaSources(List<StreamInfo> streams, long maxBitrate)
@@ -366,7 +347,7 @@ namespace MediaBrowser.Model.Dlna
/// <param name="type">The <see cref="DlnaProfileType"/>.</param>
/// <param name="playProfile">The <see cref="DirectPlayProfile"/> object to get the video stream from.</param>
/// <returns>The the normalized input container.</returns>
- public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile profile, DlnaProfileType type, DirectPlayProfile playProfile = null)
+ public static string? NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile? profile, DlnaProfileType type, DirectPlayProfile? playProfile = null)
{
if (string.IsNullOrEmpty(inputContainer))
{
@@ -394,7 +375,7 @@ namespace MediaBrowser.Model.Dlna
return formats[0];
}
- private (DirectPlayProfile Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, MediaOptions options)
+ private (DirectPlayProfile? Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, MediaOptions options)
{
var directPlayProfile = options.Profile.DirectPlayProfiles
.FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(x, item, audioStream));
@@ -410,7 +391,6 @@ namespace MediaBrowser.Model.Dlna
return (null, null, GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles));
}
- var playMethods = new List<PlayMethod>();
TranscodeReason transcodeReasons = 0;
// The profile describes what the device supports
@@ -449,7 +429,7 @@ namespace MediaBrowser.Model.Dlna
return (directPlayProfile, null, transcodeReasons);
}
- private static 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 is null ? DlnaProfileType.Audio : DlnaProfileType.Video;
@@ -575,7 +555,7 @@ namespace MediaBrowser.Model.Dlna
}
}
- private static void SetStreamInfoOptionsFromDirectPlayProfile(MediaOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile directPlayProfile)
+ private static void SetStreamInfoOptionsFromDirectPlayProfile(MediaOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile? directPlayProfile)
{
var container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile);
var protocol = "http";
@@ -587,7 +567,7 @@ namespace MediaBrowser.Model.Dlna
playlistItem.SubProtocol = protocol;
playlistItem.VideoCodecs = new[] { item.VideoStream.Codec };
- playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec);
+ playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile?.AudioCodec);
}
private StreamInfo BuildVideoItem(MediaSourceInfo item, MediaOptions options)
@@ -634,6 +614,12 @@ namespace MediaBrowser.Model.Dlna
var isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || !bitrateLimitExceeded);
TranscodeReason transcodeReasons = 0;
+ // Force transcode or remux for BD/DVD folders
+ if (item.VideoType == VideoType.Dvd || item.VideoType == VideoType.BluRay)
+ {
+ isEligibleForDirectPlay = false;
+ }
+
if (bitrateLimitExceeded)
{
transcodeReasons = TranscodeReason.ContainerBitrateExceedsLimit;
@@ -646,7 +632,7 @@ namespace MediaBrowser.Model.Dlna
isEligibleForDirectPlay,
isEligibleForDirectStream);
- DirectPlayProfile directPlayProfile = null;
+ DirectPlayProfile? directPlayProfile = null;
if (isEligibleForDirectPlay || isEligibleForDirectStream)
{
// See if it can be direct played
@@ -677,16 +663,16 @@ namespace MediaBrowser.Model.Dlna
playlistItem.AudioStreamIndex = audioStream?.Index;
if (audioStream is not null)
{
- playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec);
+ playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile?.AudioCodec);
}
SetStreamInfoOptionsFromDirectPlayProfile(options, item, playlistItem, directPlayProfile);
- BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStreams, directPlayProfile.Container, directPlayProfile.VideoCodec, directPlayProfile.AudioCodec);
+ BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStreams, directPlayProfile?.Container, directPlayProfile?.VideoCodec, directPlayProfile?.AudioCodec);
}
if (subtitleStream is not null)
{
- var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, directPlay.Value, _transcoderSupport, directPlayProfile.Container, null);
+ var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, directPlay.Value, _transcoderSupport, directPlayProfile?.Container, null);
playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method;
playlistItem.SubtitleFormat = subtitleProfile.Format;
@@ -748,7 +734,14 @@ namespace MediaBrowser.Model.Dlna
return playlistItem;
}
- private TranscodingProfile GetVideoTranscodeProfile(MediaSourceInfo item, MediaOptions options, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, MediaStream subtitleStream, StreamInfo playlistItem)
+ private TranscodingProfile? GetVideoTranscodeProfile(
+ MediaSourceInfo item,
+ MediaOptions options,
+ MediaStream? videoStream,
+ MediaStream? audioStream,
+ IEnumerable<MediaStream> candidateAudioStreams,
+ MediaStream? subtitleStream,
+ StreamInfo playlistItem)
{
if (!(item.SupportsTranscoding || item.SupportsDirectStream))
{
@@ -795,7 +788,16 @@ namespace MediaBrowser.Model.Dlna
return transcodingProfiles.FirstOrDefault();
}
- private void BuildStreamVideoItem(StreamInfo playlistItem, MediaOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, string container, string videoCodec, string audioCodec)
+ private void BuildStreamVideoItem(
+ StreamInfo playlistItem,
+ MediaOptions 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);
@@ -862,12 +864,12 @@ namespace MediaBrowser.Model.Dlna
int? bitDepth = videoStream?.BitDepth;
int? videoBitrate = videoStream?.BitRate;
double? videoLevel = videoStream?.Level;
- string videoProfile = videoStream?.Profile;
- string videoRangeType = videoStream?.VideoRangeType;
+ string? videoProfile = videoStream?.Profile;
+ string? videoRangeType = videoStream?.VideoRangeType;
float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
bool? isAnamorphic = videoStream?.IsAnamorphic;
bool? isInterlaced = videoStream?.IsInterlaced;
- string videoCodecTag = videoStream?.CodecTag;
+ string? videoCodecTag = videoStream?.CodecTag;
bool? isAvc = videoStream?.IsAVC;
TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp;
@@ -903,11 +905,11 @@ namespace MediaBrowser.Model.Dlna
playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate);
bool? isSecondaryAudio = audioStream is null ? null : item.IsSecondaryAudio(audioStream);
- int? inputAudioBitrate = audioStream is null ? null : audioStream.BitRate;
- int? audioChannels = audioStream is null ? null : audioStream.Channels;
- string audioProfile = audioStream is null ? null : audioStream.Profile;
- int? inputAudioSampleRate = audioStream is null ? null : audioStream.SampleRate;
- int? inputAudioBitDepth = audioStream is null ? null : audioStream.BitDepth;
+ int? inputAudioBitrate = audioStream?.BitRate;
+ int? audioChannels = audioStream?.Channels;
+ string? audioProfile = audioStream?.Profile;
+ int? inputAudioSampleRate = audioStream?.SampleRate;
+ int? inputAudioBitDepth = audioStream?.BitDepth;
var appliedAudioConditions = options.Profile.CodecProfiles
.Where(i => i.Type == CodecType.VideoAudio &&
@@ -955,7 +957,7 @@ namespace MediaBrowser.Model.Dlna
playlistItem?.TranscodeReasons);
}
- private static int GetDefaultAudioBitrate(string audioCodec, int? audioChannels)
+ private static int GetDefaultAudioBitrate(string? audioCodec, int? audioChannels)
{
if (!string.IsNullOrEmpty(audioCodec))
{
@@ -988,9 +990,9 @@ namespace MediaBrowser.Model.Dlna
return 192000;
}
- private static int GetAudioBitrate(long maxTotalBitrate, string[] targetAudioCodecs, MediaStream audioStream, StreamInfo item)
+ private static int GetAudioBitrate(long maxTotalBitrate, string[] targetAudioCodecs, MediaStream? audioStream, StreamInfo item)
{
- string targetAudioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0];
+ string? targetAudioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0];
int? targetAudioChannels = item.GetTargetAudioChannels(targetAudioCodec);
@@ -1081,13 +1083,13 @@ namespace MediaBrowser.Model.Dlna
return 7168000;
}
- private (DirectPlayProfile Profile, PlayMethod? PlayMethod, int? AudioStreamIndex, TranscodeReason TranscodeReasons) GetVideoDirectPlayProfile(
+ private (DirectPlayProfile? Profile, PlayMethod? PlayMethod, int? AudioStreamIndex, TranscodeReason TranscodeReasons) GetVideoDirectPlayProfile(
MediaOptions options,
MediaSourceInfo mediaSource,
- MediaStream videoStream,
- MediaStream audioStream,
+ MediaStream? videoStream,
+ MediaStream? audioStream,
ICollection<MediaStream> candidateAudioStreams,
- MediaStream subtitleStream,
+ MediaStream? subtitleStream,
bool isEligibleForDirectPlay,
bool isEligibleForDirectStream)
{
@@ -1110,12 +1112,12 @@ namespace MediaBrowser.Model.Dlna
int? bitDepth = videoStream?.BitDepth;
int? videoBitrate = videoStream?.BitRate;
double? videoLevel = videoStream?.Level;
- string videoProfile = videoStream?.Profile;
- string videoRangeType = videoStream?.VideoRangeType;
+ string? videoProfile = videoStream?.Profile;
+ string? videoRangeType = videoStream?.VideoRangeType;
float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
bool? isAnamorphic = videoStream?.IsAnamorphic;
bool? isInterlaced = videoStream?.IsInterlaced;
- string videoCodecTag = videoStream?.CodecTag;
+ string? videoCodecTag = videoStream?.CodecTag;
bool? isAvc = videoStream?.IsAVC;
TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : mediaSource.Timestamp;
@@ -1203,14 +1205,14 @@ namespace MediaBrowser.Model.Dlna
}
// Check video codec
- string videoCodec = videoStream?.Codec;
+ string? videoCodec = videoStream?.Codec;
if (!directPlayProfile.SupportsVideoCodec(videoCodec))
{
directPlayProfileReasons |= TranscodeReason.VideoCodecNotSupported;
}
// Check audio codec
- MediaStream selectedAudioStream = null;
+ MediaStream? selectedAudioStream = null;
if (candidateAudioStreams.Any())
{
selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.SupportsAudioCodec(audioStream.Codec));
@@ -1331,8 +1333,8 @@ namespace MediaBrowser.Model.Dlna
SubtitleProfile[] subtitleProfiles,
PlayMethod playMethod,
ITranscoderSupport transcoderSupport,
- string outputContainer,
- string transcodingSubProtocol)
+ string? outputContainer,
+ string? transcodingSubProtocol)
{
if (!subtitleStream.IsExternal && (playMethod != PlayMethod.Transcode || !string.Equals(transcodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase)))
{
@@ -1405,7 +1407,7 @@ namespace MediaBrowser.Model.Dlna
};
}
- private static bool IsSubtitleEmbedSupported(string transcodingContainer)
+ private static bool IsSubtitleEmbedSupported(string? transcodingContainer)
{
if (!string.IsNullOrEmpty(transcodingContainer))
{
@@ -1427,7 +1429,7 @@ namespace MediaBrowser.Model.Dlna
return false;
}
- private static SubtitleProfile GetExternalSubtitleProfile(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, ITranscoderSupport transcoderSupport, bool allowConversion)
+ private static SubtitleProfile? GetExternalSubtitleProfile(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, ITranscoderSupport transcoderSupport, bool allowConversion)
{
foreach (var profile in subtitleProfiles)
{
@@ -1560,7 +1562,7 @@ namespace MediaBrowser.Model.Dlna
private static IEnumerable<ProfileCondition> GetProfileConditionsForAudio(
IEnumerable<CodecProfile> codecProfiles,
string container,
- string codec,
+ string? codec,
int? audioChannels,
int? audioBitrate,
int? audioSampleRate,
@@ -1580,7 +1582,7 @@ namespace MediaBrowser.Model.Dlna
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)
+ private void ApplyTranscodingConditions(StreamInfo item, IEnumerable<ProfileCondition> conditions, string? qualifier, bool enableQualifiedConditions, bool enableNonQualifiedConditions)
{
foreach (ProfileCondition condition in conditions)
{
@@ -2056,7 +2058,7 @@ namespace MediaBrowser.Model.Dlna
}
// Check audio codec
- string audioCodec = audioStream?.Codec;
+ string? audioCodec = audioStream?.Codec;
if (!profile.SupportsAudioCodec(audioCodec))
{
return false;
diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs
index 93ace43df..886b64a24 100644
--- a/MediaBrowser.Model/Dlna/StreamInfo.cs
+++ b/MediaBrowser.Model/Dlna/StreamInfo.cs
@@ -107,9 +107,8 @@ namespace MediaBrowser.Model.Dlna
public string MediaSourceId => MediaSource?.Id;
- public bool IsDirectStream =>
- PlayMethod == PlayMethod.DirectStream ||
- PlayMethod == PlayMethod.DirectPlay;
+ public bool IsDirectStream => MediaSource?.VideoType is not (VideoType.Dvd or VideoType.BluRay)
+ && PlayMethod is PlayMethod.DirectStream or PlayMethod.DirectPlay;
/// <summary>
/// Gets the audio stream that will be used.
diff --git a/MediaBrowser.Model/Globalization/ILocalizationManager.cs b/MediaBrowser.Model/Globalization/ILocalizationManager.cs
index e00157dce..02a29e7fa 100644
--- a/MediaBrowser.Model/Globalization/ILocalizationManager.cs
+++ b/MediaBrowser.Model/Globalization/ILocalizationManager.cs
@@ -30,8 +30,9 @@ namespace MediaBrowser.Model.Globalization
/// Gets the rating level.
/// </summary>
/// <param name="rating">The rating.</param>
+ /// <param name="countryCode">The optional two letter ISO language string.</param>
/// <returns><see cref="int" /> or <c>null</c>.</returns>
- int? GetRatingLevel(string rating);
+ int? GetRatingLevel(string rating, string? countryCode = null);
/// <summary>
/// Gets the localized string.
diff --git a/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs b/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs
new file mode 100644
index 000000000..d546ffccd
--- /dev/null
+++ b/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs
@@ -0,0 +1,41 @@
+#nullable disable
+
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Model.MediaInfo;
+
+/// <summary>
+/// Represents the result of BDInfo output.
+/// </summary>
+public class BlurayDiscInfo
+{
+ /// <summary>
+ /// Gets or sets the media streams.
+ /// </summary>
+ /// <value>The media streams.</value>
+ public MediaStream[] MediaStreams { get; set; }
+
+ /// <summary>
+ /// Gets or sets the run time ticks.
+ /// </summary>
+ /// <value>The run time ticks.</value>
+ public long? RunTimeTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the files.
+ /// </summary>
+ /// <value>The files.</value>
+ public string[] Files { get; set; }
+
+ /// <summary>
+ /// Gets or sets the playlist name.
+ /// </summary>
+ /// <value>The playlist name.</value>
+ public string PlaylistName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the chapters.
+ /// </summary>
+ /// <value>The chapters.</value>
+ public double[] Chapters { get; set; }
+}
diff --git a/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs b/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs
new file mode 100644
index 000000000..d39725301
--- /dev/null
+++ b/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs
@@ -0,0 +1,14 @@
+namespace MediaBrowser.Model.MediaInfo;
+
+/// <summary>
+/// Interface IBlurayExaminer.
+/// </summary>
+public interface IBlurayExaminer
+{
+ /// <summary>
+ /// Gets the disc info.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>BlurayDiscInfo.</returns>
+ BlurayDiscInfo GetDiscInfo(string path);
+}
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index 0f35c6a5e..213639371 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -36,6 +36,7 @@ namespace MediaBrowser.Providers.MediaInfo
private readonly ILogger<FFProbeVideoInfo> _logger;
private readonly IMediaEncoder _mediaEncoder;
private readonly IItemRepository _itemRepo;
+ private readonly IBlurayExaminer _blurayExaminer;
private readonly ILocalizationManager _localization;
private readonly IEncodingManager _encodingManager;
private readonly IServerConfigurationManager _config;
@@ -51,6 +52,7 @@ namespace MediaBrowser.Providers.MediaInfo
IMediaSourceManager mediaSourceManager,
IMediaEncoder mediaEncoder,
IItemRepository itemRepo,
+ IBlurayExaminer blurayExaminer,
ILocalizationManager localization,
IEncodingManager encodingManager,
IServerConfigurationManager config,
@@ -64,6 +66,7 @@ namespace MediaBrowser.Providers.MediaInfo
_mediaSourceManager = mediaSourceManager;
_mediaEncoder = mediaEncoder;
_itemRepo = itemRepo;
+ _blurayExaminer = blurayExaminer;
_localization = localization;
_encodingManager = encodingManager;
_config = config;
@@ -80,16 +83,77 @@ namespace MediaBrowser.Providers.MediaInfo
CancellationToken cancellationToken)
where T : Video
{
+ BlurayDiscInfo blurayDiscInfo = null;
+
Model.MediaInfo.MediaInfo mediaInfoResult = null;
if (!item.IsShortcut || options.EnableRemoteContentProbe)
{
- mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false);
+ if (item.VideoType == VideoType.Dvd)
+ {
+ // Get list of playable .vob files
+ var vobs = _mediaEncoder.GetPrimaryPlaylistVobFiles(item.Path, null);
+
+ // Return if no playable .vob files are found
+ if (vobs.Count == 0)
+ {
+ _logger.LogError("No playable .vob files found in DVD structure, skipping FFprobe.");
+ return ItemUpdateType.MetadataImport;
+ }
+
+ // Fetch metadata of first .vob file
+ mediaInfoResult = await GetMediaInfo(
+ new Video
+ {
+ Path = vobs[0]
+ },
+ cancellationToken).ConfigureAwait(false);
+
+ // Sum up the runtime of all .vob files skipping the first .vob
+ for (var i = 1; i < vobs.Count; i++)
+ {
+ var tmpMediaInfo = await GetMediaInfo(
+ new Video
+ {
+ Path = vobs[i]
+ },
+ cancellationToken).ConfigureAwait(false);
+
+ mediaInfoResult.RunTimeTicks += tmpMediaInfo.RunTimeTicks;
+ }
+ }
+ else if (item.VideoType == VideoType.BluRay)
+ {
+ // Get BD disc information
+ blurayDiscInfo = GetBDInfo(item.Path);
+
+ // Get playable .m2ts files
+ var m2ts = _mediaEncoder.GetPrimaryPlaylistM2tsFiles(item.Path);
+
+ // Return if no playable .m2ts files are found
+ if (blurayDiscInfo.Files.Length == 0 || m2ts.Count == 0)
+ {
+ _logger.LogError("No playable .m2ts files found in Blu-ray structure, skipping FFprobe.");
+ return ItemUpdateType.MetadataImport;
+ }
+
+ // Fetch metadata of first .m2ts file
+ mediaInfoResult = await GetMediaInfo(
+ new Video
+ {
+ Path = m2ts[0]
+ },
+ cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false);
+ }
cancellationToken.ThrowIfCancellationRequested();
}
- await Fetch(item, cancellationToken, mediaInfoResult, options).ConfigureAwait(false);
+ await Fetch(item, cancellationToken, mediaInfoResult, blurayDiscInfo, options).ConfigureAwait(false);
return ItemUpdateType.MetadataImport;
}
@@ -129,6 +193,7 @@ namespace MediaBrowser.Providers.MediaInfo
Video video,
CancellationToken cancellationToken,
Model.MediaInfo.MediaInfo mediaInfo,
+ BlurayDiscInfo blurayInfo,
MetadataRefreshOptions options)
{
List<MediaStream> mediaStreams;
@@ -153,19 +218,8 @@ namespace MediaBrowser.Providers.MediaInfo
}
mediaAttachments = mediaInfo.MediaAttachments;
-
video.TotalBitrate = mediaInfo.Bitrate;
- // video.FormatName = (mediaInfo.Container ?? string.Empty)
- // .Replace("matroska", "mkv", StringComparison.OrdinalIgnoreCase);
-
- // For DVDs this may not always be accurate, so don't set the runtime if the item already has one
- var needToSetRuntime = video.VideoType != VideoType.Dvd || video.RunTimeTicks is null || video.RunTimeTicks.Value == 0;
-
- if (needToSetRuntime)
- {
- video.RunTimeTicks = mediaInfo.RunTimeTicks;
- }
-
+ video.RunTimeTicks = mediaInfo.RunTimeTicks;
video.Size = mediaInfo.Size;
if (video.VideoType == VideoType.VideoFile)
@@ -182,6 +236,10 @@ namespace MediaBrowser.Providers.MediaInfo
video.Container = mediaInfo.Container;
chapters = mediaInfo.Chapters ?? Array.Empty<ChapterInfo>();
+ if (blurayInfo is not null)
+ {
+ FetchBdInfo(video, ref chapters, mediaStreams, blurayInfo);
+ }
}
else
{
@@ -240,7 +298,7 @@ namespace MediaBrowser.Providers.MediaInfo
if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh ||
options.MetadataRefreshMode == MetadataRefreshMode.Default)
{
- if (chapters.Length == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video))
+ if (_config.Configuration.DummyChapterDuration > 0 && chapters.Length == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video))
{
chapters = CreateDummyChapters(video);
}
@@ -277,6 +335,86 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
+ private void FetchBdInfo(Video video, ref ChapterInfo[] chapters, List<MediaStream> mediaStreams, BlurayDiscInfo blurayInfo)
+ {
+ if (blurayInfo.Files.Length <= 1)
+ {
+ return;
+ }
+
+ // Use BD Info if it has multiple m2ts. Otherwise, treat it like a video file and rely more on ffprobe output
+ int? currentHeight = null;
+ int? currentWidth = null;
+ int? currentBitRate = null;
+
+ var videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video);
+
+ // Grab the values that ffprobe recorded
+ if (videoStream is not null)
+ {
+ currentBitRate = videoStream.BitRate;
+ currentWidth = videoStream.Width;
+ currentHeight = videoStream.Height;
+ }
+
+ // Fill video properties from the BDInfo result
+ mediaStreams.Clear();
+ mediaStreams.AddRange(blurayInfo.MediaStreams);
+
+ if (blurayInfo.RunTimeTicks.HasValue && blurayInfo.RunTimeTicks.Value > 0)
+ {
+ video.RunTimeTicks = blurayInfo.RunTimeTicks;
+ }
+
+ if (blurayInfo.Chapters is not null)
+ {
+ double[] brChapter = blurayInfo.Chapters;
+ chapters = new ChapterInfo[brChapter.Length];
+ for (int i = 0; i < brChapter.Length; i++)
+ {
+ chapters[i] = new ChapterInfo
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(brChapter[i]).Ticks
+ };
+ }
+ }
+
+ videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video);
+
+ // Use the ffprobe values if these are empty
+ if (videoStream is not null)
+ {
+ videoStream.BitRate = IsEmpty(videoStream.BitRate) ? currentBitRate : videoStream.BitRate;
+ videoStream.Width = IsEmpty(videoStream.Width) ? currentWidth : videoStream.Width;
+ videoStream.Height = IsEmpty(videoStream.Height) ? currentHeight : videoStream.Height;
+ }
+ }
+
+ private bool IsEmpty(int? num)
+ {
+ return !num.HasValue || num.Value == 0;
+ }
+
+ /// <summary>
+ /// Gets information about the longest playlist on a bdrom.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>VideoStream.</returns>
+ private BlurayDiscInfo GetBDInfo(string path)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(path);
+
+ try
+ {
+ return _blurayExaminer.GetDiscInfo(path);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting BDInfo");
+ return null;
+ }
+ }
+
private void FetchEmbeddedInfo(Video video, Model.MediaInfo.MediaInfo data, MetadataRefreshOptions refreshOptions, LibraryOptions libraryOptions)
{
var replaceData = refreshOptions.ReplaceAllMetadata;
@@ -524,39 +662,39 @@ namespace MediaBrowser.Providers.MediaInfo
private ChapterInfo[] CreateDummyChapters(Video video)
{
var runtime = video.RunTimeTicks ?? 0;
- long dummyChapterDuration = TimeSpan.FromSeconds(_config.Configuration.DummyChapterDuration).Ticks;
- if (runtime < 0)
+ // Only process files with a runtime higher than 0 and lower than 12h. The latter are likely corrupted.
+ if (runtime < 0 || runtime > TimeSpan.FromHours(12).Ticks)
{
throw new ArgumentException(
string.Format(
CultureInfo.InvariantCulture,
- "{0} has invalid runtime of {1}",
+ "{0} has an invalid runtime of {1} minutes",
video.Name,
- runtime));
+ TimeSpan.FromTicks(runtime).Minutes));
}
- if (runtime < dummyChapterDuration)
+ long dummyChapterDuration = TimeSpan.FromSeconds(_config.Configuration.DummyChapterDuration).Ticks;
+ if (runtime > dummyChapterDuration)
{
- return Array.Empty<ChapterInfo>();
- }
-
- // Limit the chapters just in case there's some incorrect metadata here
- int chapterCount = (int)Math.Min(runtime / dummyChapterDuration, _config.Configuration.DummyChapterCount);
- var chapters = new ChapterInfo[chapterCount];
+ int chapterCount = (int)(runtime / dummyChapterDuration);
+ var chapters = new ChapterInfo[chapterCount];
- long currentChapterTicks = 0;
- for (int i = 0; i < chapterCount; i++)
- {
- chapters[i] = new ChapterInfo
+ long currentChapterTicks = 0;
+ for (int i = 0; i < chapterCount; i++)
{
- StartPositionTicks = currentChapterTicks
- };
+ chapters[i] = new ChapterInfo
+ {
+ StartPositionTicks = currentChapterTicks
+ };
+
+ currentChapterTicks += dummyChapterDuration;
+ }
- currentChapterTicks += dummyChapterDuration;
+ return chapters;
}
- return chapters;
+ return Array.Empty<ChapterInfo>();
}
}
}
diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs
index 31fa3da1c..280021955 100644
--- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs
@@ -53,6 +53,7 @@ namespace MediaBrowser.Providers.MediaInfo
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
/// <param name="itemRepo">Instance of the <see cref="IItemRepository"/> interface.</param>
+ /// <param name="blurayExaminer">Instance of the <see cref="IBlurayExaminer"/> interface.</param>
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
/// <param name="encodingManager">Instance of the <see cref="IEncodingManager"/> interface.</param>
/// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
@@ -66,6 +67,7 @@ namespace MediaBrowser.Providers.MediaInfo
IMediaSourceManager mediaSourceManager,
IMediaEncoder mediaEncoder,
IItemRepository itemRepo,
+ IBlurayExaminer blurayExaminer,
ILocalizationManager localization,
IEncodingManager encodingManager,
IServerConfigurationManager config,
@@ -85,6 +87,7 @@ namespace MediaBrowser.Providers.MediaInfo
mediaSourceManager,
mediaEncoder,
itemRepo,
+ blurayExaminer,
localization,
encodingManager,
config,
diff --git a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
index 0fb9d30a6..ae244da19 100644
--- a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
@@ -53,7 +53,7 @@ namespace MediaBrowser.Providers.Plugins.StudioImages
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
- return new List<ImageType>
+ return new ImageType[]
{
ImageType.Thumb
};
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
index 159b8d658..8bd30447a 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -274,16 +274,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var val = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(val))
+ if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added))
{
- if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added))
- {
- item.DateCreated = added;
- }
- else
- {
- Logger.LogWarning("Invalid Added value found: {Value}", val);
- }
+ item.DateCreated = added;
+ }
+ else
+ {
+ Logger.LogWarning("Invalid Added value found: {Value}", val);
}
break;
@@ -376,15 +373,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers
case "playcount":
{
var val = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(val) && !string.IsNullOrWhiteSpace(nfoConfiguration.UserId))
+ if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var count)
+ && Guid.TryParse(nfoConfiguration.UserId, out var guid))
{
- if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var count))
- {
- var user = _userManager.GetUserById(Guid.Parse(nfoConfiguration.UserId));
- userData = _userDataManager.GetUserData(user, item);
- userData.PlayCount = count;
- _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
- }
+ var user = _userManager.GetUserById(guid);
+ userData = _userDataManager.GetUserData(user, item);
+ userData.PlayCount = count;
+ _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
}
break;
@@ -393,11 +388,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers
case "lastplayed":
{
var val = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(val) && !string.IsNullOrWhiteSpace(nfoConfiguration.UserId))
+ if (Guid.TryParse(nfoConfiguration.UserId, out var guid))
{
if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added))
{
- var user = _userManager.GetUserById(Guid.Parse(nfoConfiguration.UserId));
+ var user = _userManager.GetUserById(guid);
userData = _userDataManager.GetUserData(user, item);
userData.LastPlayedDate = added;
_userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
@@ -487,12 +482,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var text = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(text))
+ if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
{
- if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
- {
- item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
- }
+ item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
}
break;
@@ -630,13 +622,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var val = reader.ReadElementContentAsString();
- var hasDisplayOrder = item as IHasDisplayOrder;
- if (hasDisplayOrder is not null)
+ if (item is IHasDisplayOrder hasDisplayOrder && !string.IsNullOrWhiteSpace(val))
{
- if (!string.IsNullOrWhiteSpace(val))
- {
- hasDisplayOrder.DisplayOrder = val;
- }
+ hasDisplayOrder.DisplayOrder = val;
}
break;
@@ -646,12 +634,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var val = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(val))
+ if (int.TryParse(val, out var productionYear) && productionYear > 1850)
{
- if (int.TryParse(val, out var productionYear) && productionYear > 1850)
- {
- item.ProductionYear = productionYear;
- }
+ item.ProductionYear = productionYear;
}
break;
@@ -661,13 +646,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var rating = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(rating))
+ // All external meta is saving this as '.' for decimal I believe...but just to be sure
+ if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val))
{
- // All external meta is saving this as '.' for decimal I believe...but just to be sure
- if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val))
- {
- item.CommunityRating = val;
- }
+ item.CommunityRating = val;
}
break;
@@ -697,13 +679,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers
var val = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(val))
+ if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
{
- if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
- {
- item.PremiereDate = date;
- item.ProductionYear = date.Year;
- }
+ item.PremiereDate = date;
+ item.ProductionYear = date.Year;
}
break;
@@ -715,12 +694,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
var val = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(val))
+ if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
{
- if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
- {
- item.EndDate = date;
- }
+ item.EndDate = date;
}
break;
@@ -1191,21 +1167,21 @@ namespace MediaBrowser.XbmcMetadata.Parsers
case "value":
var val = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(val))
+ if (float.TryParse(val, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var ratingValue))
{
- if (float.TryParse(val, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var ratingValue))
+ // if ratingName contains tomato --> assume critic rating
+ if (ratingName is not null
+ && ratingName.Contains("tomato", StringComparison.OrdinalIgnoreCase)
+ && !ratingName.Contains("audience", StringComparison.OrdinalIgnoreCase))
{
- // if ratingName contains tomato --> assume critic rating
- if (ratingName is not null &&
- ratingName.Contains("tomato", StringComparison.OrdinalIgnoreCase) &&
- !ratingName.Contains("audience", StringComparison.OrdinalIgnoreCase))
+ if (!ratingName.Contains("avg", StringComparison.OrdinalIgnoreCase))
{
item.CriticRating = ratingValue;
}
- else
- {
- item.CommunityRating = ratingValue;
- }
+ }
+ else
+ {
+ item.CommunityRating = ratingValue;
}
}
@@ -1289,12 +1265,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var val = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(val))
+ if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal))
{
- if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal))
- {
- sortOrder = intVal;
- }
+ sortOrder = intVal;
}
break;
diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64
index 95b08eb05..36435194f 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/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-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 18fb7bebe..c8943a399 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 make
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-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 e0555cd22..7b9a3de4e 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/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-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 ad5a0890b..32695e3f1 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/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-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 2d8be1835..8ffbeafad 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/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-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/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
index 1b27e344b..db7e91c6a 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
@@ -17,6 +17,8 @@ namespace Jellyfin.MediaEncoding.Tests
}
[Theory]
+ [InlineData(EncoderValidatorTestsData.FFmpegV60Output, true)]
+ [InlineData(EncoderValidatorTestsData.FFmpegV512Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV44Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV432Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV431Output, true)]
@@ -36,6 +38,8 @@ namespace Jellyfin.MediaEncoding.Tests
{
public GetFFmpegVersionTestData()
{
+ Add(EncoderValidatorTestsData.FFmpegV60Output, new Version(6, 0));
+ Add(EncoderValidatorTestsData.FFmpegV512Output, new Version(5, 1, 2));
Add(EncoderValidatorTestsData.FFmpegV44Output, new Version(4, 4));
Add(EncoderValidatorTestsData.FFmpegV432Output, new Version(4, 3, 2));
Add(EncoderValidatorTestsData.FFmpegV431Output, new Version(4, 3, 1));
diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
index 02bf046ed..89ba42da0 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
@@ -2,6 +2,30 @@ namespace Jellyfin.MediaEncoding.Tests
{
internal static class EncoderValidatorTestsData
{
+ public const string FFmpegV60Output = @"ffmpeg version 6.0-Jellyfin Copyright (c) 2000-2023 the FFmpeg developers
+built with gcc 12.2.0 (crosstool-NG 1.25.0.90_cf9beb1)
+configuration: --prefix=/ffbuild/prefix --pkg-config=pkg-config --pkg-config-flags=--static --cross-prefix=x86_64-w64-mingw32- --arch=x86_64 --target-os=mingw32 --extra-version=Jellyfin --extra-cflags= --extra-cxxflags= --extra-ldflags= --extra-ldexeflags= --extra-libs= --enable-gpl --enable-version3 --enable-lto --disable-ffplay --disable-debug --disable-doc --disable-ptx-compression --disable-sdl2 --disable-w32threads --enable-pthreads --enable-iconv --enable-libxml2 --enable-zlib --enable-libfreetype --enable-libfribidi --enable-gmp --enable-lzma --enable-fontconfig --enable-libvorbis --enable-opencl --enable-amf --enable-chromaprint --enable-libdav1d --enable-dxva2 --enable-d3d11va --enable-libfdk-aac --enable-ffnvcodec --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvdec --enable-nvenc --enable-libass --enable-libbluray --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvpx --enable-libwebp --enable-libvpl --enable-schannel --enable-libsrt --enable-libsvtav1 --enable-vulkan --enable-libshaderc --enable-libplacebo --enable-libx264 --enable-libx265 --enable-libzimg --enable-libzvbi
+libavutil 58. 2.100 / 58. 2.100
+libavcodec 60. 3.100 / 60. 3.100
+libavformat 60. 3.100 / 60. 3.100
+libavdevice 60. 1.100 / 60. 1.100
+libavfilter 9. 3.100 / 9. 3.100
+libswscale 7. 1.100 / 7. 1.100
+libswresample 4. 10.100 / 4. 10.100
+libpostproc 57. 1.100 / 57. 1.100";
+
+ public const string FFmpegV512Output = @"ffmpeg version 5.1.2-Jellyfin Copyright (c) 2000-2022 the FFmpeg developers
+built with gcc 10-win32 (GCC) 20220324
+configuration: --prefix=/opt/ffmpeg --arch=x86_64 --target-os=mingw32 --cross-prefix=x86_64-w64-mingw32- --pkg-config=pkg-config --pkg-config-flags=--static --extra-libs='-lfftw3f -lstdc++' --extra-cflags=-DCHROMAPRINT_NODLL --extra-version=Jellyfin --disable-ffplay --disable-debug --disable-doc --disable-sdl2 --disable-ptx-compression --disable-w32threads --enable-pthreads --enable-shared --enable-lto --enable-gpl --enable-version3 --enable-schannel --enable-iconv --enable-libxml2 --enable-zlib --enable-lzma --enable-gmp --enable-chromaprint --enable-libfreetype --enable-libfribidi --enable-libfontconfig --enable-libass --enable-libbluray --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libvpx --enable-libzimg --enable-libx264 --enable-libx265 --enable-libsvtav1 --enable-libdav1d --enable-libfdk-aac --enable-opencl --enable-dxva2 --enable-d3d11va --enable-amf --enable-libmfx --enable-ffnvcodec --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvdec --enable-nvenc
+libavutil 57. 28.100 / 57. 28.100
+libavcodec 59. 37.100 / 59. 37.100
+libavformat 59. 27.100 / 59. 27.100
+libavdevice 59. 7.100 / 59. 7.100
+libavfilter 8. 44.100 / 8. 44.100
+libswscale 6. 7.100 / 6. 7.100
+libswresample 4. 7.100 / 4. 7.100
+libpostproc 56. 6.100 / 56. 6.100";
+
public const string FFmpegV44Output = @"ffmpeg version 4.4-Jellyfin Copyright (c) 2000-2021 the FFmpeg developers
built with gcc 10.3.0 (Rev5, Built by MSYS2 project)
configuration: --disable-static --enable-shared --extra-version=Jellyfin --disable-ffplay --disable-debug --enable-gpl --enable-version3 --enable-bzlib --enable-iconv --enable-lzma --enable-zlib --enable-sdl2 --enable-fontconfig --enable-gmp --enable-libass --enable-libzimg --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libvpx --enable-libx264 --enable-libx265 --enable-libdav1d --enable-opencl --enable-dxva2 --enable-d3d11va --enable-amf --enable-libmfx --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvenc --enable-nvdec --enable-ffnvcodec --enable-gnutls
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs
new file mode 100644
index 000000000..d136c1bc6
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs
@@ -0,0 +1,76 @@
+using System.Linq;
+using Emby.Naming.Common;
+using Emby.Server.Implementations.Library.Resolvers.Audio;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.IO;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Library;
+
+public class AudioResolverTests
+{
+ private static readonly NamingOptions _namingOptions = new();
+
+ [Theory]
+ [InlineData("words.mp3")] // single non-tagged file
+ [InlineData("chapter 01.mp3")]
+ [InlineData("part 1.mp3")]
+ [InlineData("chapter 01.mp3", "non-media.txt")]
+ [InlineData("title.mp3", "title.epub")]
+ [InlineData("01.mp3", "subdirectory/")] // single media file with sub-directory - note that this will hide any contents in the subdirectory
+ public void Resolve_AudiobookDirectory_SingleResult(params string[] children)
+ {
+ var resolved = TestResolveChildren("/parent/title", children);
+ Assert.NotNull(resolved);
+ }
+
+ [Theory]
+ /* Results that can't be displayed as an audio book. */
+ [InlineData] // no contents
+ [InlineData("subdirectory/")]
+ [InlineData("non-media.txt")]
+ /* Names don't indicate parts of a single book. */
+ [InlineData("Name.mp3", "Another Name.mp3")]
+ /* Results that are an audio book but not currently navigable as such (multiple chapters and/or parts). */
+ [InlineData("01.mp3", "02.mp3")]
+ [InlineData("chapter 01.mp3", "chapter 02.mp3")]
+ [InlineData("part 1.mp3", "part 2.mp3")]
+ [InlineData("chapter 01 part 01.mp3", "chapter 01 part 02.mp3")]
+ /* Mismatched chapters, parts, and named files. */
+ [InlineData("chapter 01.mp3", "part 2.mp3")]
+ [InlineData("book title.mp3", "chapter name.mp3")] // "book title" resolves as alternate version of book based on directory name
+ [InlineData("01 Content.mp3", "01 Credits.mp3")] // resolves as alternate versions of chapter 1
+ [InlineData("Chapter Name.mp3", "Part 1.mp3")]
+ public void Resolve_AudiobookDirectory_NoResult(params string[] children)
+ {
+ var resolved = TestResolveChildren("/parent/book title", children);
+ Assert.Null(resolved);
+ }
+
+ private Audio? TestResolveChildren(string parent, string[] children)
+ {
+ var childrenMetadata = children.Select(name => new FileSystemMetadata
+ {
+ FullName = parent + "/" + name,
+ IsDirectory = name.EndsWith('/')
+ }).ToArray();
+
+ var resolver = new AudioResolver(_namingOptions);
+ var itemResolveArgs = new ItemResolveArgs(
+ null,
+ Mock.Of<ILibraryManager>())
+ {
+ CollectionType = "books",
+ FileInfo = new FileSystemMetadata
+ {
+ FullName = parent,
+ IsDirectory = true
+ },
+ FileSystemChildren = childrenMetadata
+ };
+
+ return resolver.Resolve(itemResolveArgs);
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
index 286ba0405..6d0ed7bbb 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
@@ -22,10 +22,10 @@ namespace Jellyfin.Server.Implementations.Tests.Library
{
var parent = new Folder { Name = "extras" };
- var episodeResolver = new EpisodeResolver(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions);
+ var episodeResolver = new EpisodeResolver(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions, Mock.Of<IDirectoryService>());
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
- Mock.Of<IDirectoryService>())
+ null)
{
Parent = parent,
CollectionType = CollectionType.TvShows,
@@ -45,10 +45,10 @@ 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(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions);
+ var episodeResolver = new EpisodeResolverMock(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions, Mock.Of<IDirectoryService>());
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
- Mock.Of<IDirectoryService>())
+ null)
{
Parent = series,
CollectionType = CollectionType.TvShows,
@@ -62,7 +62,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
private sealed class EpisodeResolverMock : EpisodeResolver
{
- public EpisodeResolverMock(ILogger<EpisodeResolver> logger, NamingOptions namingOptions) : base(logger, namingOptions)
+ public EpisodeResolverMock(ILogger<EpisodeResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService) : base(logger, namingOptions, directoryService)
{
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs
index efc3ac0c2..aed584355 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs
@@ -18,10 +18,10 @@ public class MovieResolverTests
[Fact]
public void Resolve_GivenLocalAlternateVersion_ResolvesToVideo()
{
- var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions);
+ var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions, Mock.Of<IDirectoryService>());
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
- Mock.Of<IDirectoryService>())
+ null)
{
Parent = null,
FileInfo = new FileSystemMetadata
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
index ab3682ccf..7fabe9904 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
@@ -83,7 +83,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
await localizationManager.LoadAll();
var ratings = localizationManager.GetParentalRatings().ToList();
- Assert.Equal(53, ratings.Count);
+ Assert.Equal(54, ratings.Count);
var tvma = ratings.FirstOrDefault(x => x.Name.Equals("TV-MA", StringComparison.Ordinal));
Assert.NotNull(tvma);
@@ -100,7 +100,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
await localizationManager.LoadAll();
var ratings = localizationManager.GetParentalRatings().ToList();
- Assert.Equal(18, ratings.Count);
+ Assert.Equal(19, ratings.Count);
var fsk = ratings.FirstOrDefault(x => x.Name.Equals("FSK-12", StringComparison.Ordinal));
Assert.NotNull(fsk);