aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/codeql-analysis.yml4
-rw-r--r--.github/workflows/commands.yml4
-rw-r--r--.github/workflows/openapi.yml8
-rw-r--r--.github/workflows/repo-stale.yaml2
-rw-r--r--CONTRIBUTORS.md4
-rw-r--r--Dockerfile11
-rw-r--r--Emby.Dlna/PlayTo/Device.cs14
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs2
-rw-r--r--Emby.Dlna/PlayTo/TransportState.cs13
-rw-r--r--Emby.Naming/Common/NamingOptions.cs286
-rw-r--r--Emby.Naming/ExternalFiles/ExternalPathParser.cs116
-rw-r--r--Emby.Naming/ExternalFiles/ExternalPathParserResult.cs (renamed from Emby.Naming/Subtitles/SubtitleInfo.cs)20
-rw-r--r--Emby.Naming/Subtitles/SubtitleParser.cs71
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj2
-rw-r--r--Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs2
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs12
-rw-r--r--Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Images/DynamicImageProvider.cs10
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs9
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs31
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs8
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs8
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs6
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs6
-rw-r--r--Emby.Server.Implementations/Localization/Core/cs.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/cy.json54
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-US.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/eo.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-MX.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/fa.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/fi.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/hi.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/hu.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/kk.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/mr.json44
-rw-r--r--Emby.Server.Implementations/Localization/Core/pr.json15
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/sv.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ta.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/vi.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-CN.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-HK.json2
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/au.csv1
-rw-r--r--Emby.Server.Implementations/Net/SocketFactory.cs20
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs7
-rw-r--r--Emby.Server.Implementations/Sorting/IndexNumberComparer.cs50
-rw-r--r--Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs50
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs15
-rw-r--r--Jellyfin.Api/Controllers/DisplayPreferencesController.cs6
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs17
-rw-r--r--Jellyfin.Api/Controllers/HlsSegmentController.cs4
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs54
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs71
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs6
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs6
-rw-r--r--Jellyfin.Api/Helpers/AudioHelper.cs6
-rw-r--r--Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs22
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs25
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj2
-rw-r--r--Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs40
-rw-r--r--Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs18
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj8
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj8
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs13
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs12
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs179
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs6
-rw-r--r--MediaBrowser.Controller/Session/SessionInfo.cs6
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs124
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs15
-rw-r--r--MediaBrowser.MediaEncoding/Probing/MediaChapter.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs26
-rw-r--r--MediaBrowser.Model/Dlna/DlnaProfileType.cs3
-rw-r--r--MediaBrowser.Model/Entities/MediaStream.cs18
-rw-r--r--MediaBrowser.Model/IO/IFileSystem.cs14
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj2
-rw-r--r--MediaBrowser.Model/Net/ISocketFactory.cs2
-rw-r--r--MediaBrowser.Model/Querying/ItemSortBy.cs4
-rw-r--r--MediaBrowser.Model/Querying/NextUpQuery.cs4
-rw-r--r--MediaBrowser.Model/Session/GeneralCommand.cs26
-rw-r--r--MediaBrowser.Model/Session/HardwareEncodingType.cs18
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioResolver.cs166
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs20
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs29
-rw-r--r--MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs232
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs243
-rw-r--r--MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs45
-rw-r--r--RSSDP/SsdpCommunicationsServer.cs2
-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--src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs4
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj2
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj6
-rw-r--r--tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj2
-rw-r--r--tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj4
-rw-r--r--tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj4
-rw-r--r--tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj2
-rw-r--r--tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj2
-rw-r--r--tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj2
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj4
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs10
-rw-r--r--tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj2
-rw-r--r--tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs111
-rw-r--r--tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj3
-rw-r--r--tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs41
-rw-r--r--tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj4
-rw-r--r--tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj4
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/AudioResolverTests.cs86
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs435
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs165
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj4
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs9
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs2
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs3
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj6
-rw-r--r--tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj6
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj4
124 files changed, 2102 insertions, 1321 deletions
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index ea1d30cdf..a648093ed 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -20,9 +20,9 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Setup .NET Core
- uses: actions/setup-dotnet@v1
+ uses: actions/setup-dotnet@v2
with:
dotnet-version: '6.0.x'
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index af4d8beb9..730ac7f46 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -23,7 +23,7 @@ jobs:
reactions: '+1'
- name: Checkout the latest code
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@@ -47,7 +47,7 @@ jobs:
reactions: eyes
- name: Checkout the latest code
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml
index eb2e98070..e7ac59ea6 100644
--- a/.github/workflows/openapi.yml
+++ b/.github/workflows/openapi.yml
@@ -12,12 +12,12 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET Core
- uses: actions/setup-dotnet@v1
+ uses: actions/setup-dotnet@v2
with:
dotnet-version: '6.0.x'
- name: Generate openapi.json
@@ -37,11 +37,11 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
ref: ${{ github.base_ref }}
- name: Setup .NET Core
- uses: actions/setup-dotnet@v1
+ uses: actions/setup-dotnet@v2
with:
dotnet-version: '6.0.x'
- name: Generate openapi.json
diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml
index 63ded4140..0504b1c50 100644
--- a/.github/workflows/repo-stale.yaml
+++ b/.github/workflows/repo-stale.yaml
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- - uses: actions/stale@v4.1.0
+ - uses: actions/stale@v5
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
days-before-stale: 120
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 86a8ecc82..f23ecf942 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -45,6 +45,7 @@
- [Froghut](https://github.com/Froghut)
- [fruhnow](https://github.com/fruhnow)
- [geilername](https://github.com/geilername)
+ - [GermanCoding](https://github.com/GermanCoding)
- [gnattu](https://github.com/gnattu)
- [GodTamIt](https://github.com/GodTamIt)
- [grafixeyehero](https://github.com/grafixeyehero)
@@ -76,6 +77,7 @@
- [mitchfizz05](https://github.com/mitchfizz05)
- [MrTimscampi](https://github.com/MrTimscampi)
- [n8225](https://github.com/n8225)
+ - [Nalsai](https://github.com/Nalsai)
- [Narfinger](https://github.com/Narfinger)
- [NathanPickard](https://github.com/NathanPickard)
- [neilsb](https://github.com/neilsb)
@@ -115,6 +117,7 @@
- [ssenart](https://github.com/ssenart)
- [stanionascu](https://github.com/stanionascu)
- [stevehayles](https://github.com/stevehayles)
+ - [StollD](https://github.com/StollD)
- [SuperSandro2000](https://github.com/SuperSandro2000)
- [tbraeutigam](https://github.com/tbraeutigam)
- [teacupx](https://github.com/teacupx)
@@ -152,6 +155,7 @@
- [MBR-0001](https://github.com/MBR-0001)
- [jonas-resch](https://github.com/jonas-resch)
- [vgambier](https://github.com/vgambier)
+ - [MinecraftPlaye](https://github.com/MinecraftPlaye)
# Emby Contributors
diff --git a/Dockerfile b/Dockerfile
index f0090889f..c3038b1d2 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -22,10 +22,10 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
# https://github.com/intel/compute-runtime/releases
-ARG GMMLIB_VERSION=21.2.1
-ARG IGC_VERSION=1.0.8517
-ARG NEO_VERSION=21.35.20826
-ARG LEVEL_ZERO_VERSION=1.2.20826
+ARG GMMLIB_VERSION=22.0.2
+ARG IGC_VERSION=1.0.10395
+ARG NEO_VERSION=22.08.22549
+ARG LEVEL_ZERO_VERSION=1.3.22549
# Install dependencies:
# mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding.
@@ -48,8 +48,7 @@ RUN apt-get update \
&& wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-gmmlib_${GMMLIB_VERSION}_amd64.deb \
&& wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-core_${IGC_VERSION}_amd64.deb \
&& wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-opencl_${IGC_VERSION}_amd64.deb \
- && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-opencl_${NEO_VERSION}_amd64.deb \
- && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-ocloc_${NEO_VERSION}_amd64.deb \
+ && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-opencl-icd_${NEO_VERSION}_amd64.deb \
&& wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-level-zero-gpu_${LEVEL_ZERO_VERSION}_amd64.deb \
&& dpkg -i *.deb \
&& cd .. \
diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs
index 7815e9293..8eb90f445 100644
--- a/Emby.Dlna/PlayTo/Device.cs
+++ b/Emby.Dlna/PlayTo/Device.cs
@@ -69,11 +69,11 @@ namespace Emby.Dlna.PlayTo
public TransportState TransportState { get; private set; }
- public bool IsPlaying => TransportState == TransportState.Playing;
+ public bool IsPlaying => TransportState == TransportState.PLAYING;
- public bool IsPaused => TransportState == TransportState.Paused || TransportState == TransportState.PausedPlayback;
+ public bool IsPaused => TransportState == TransportState.PAUSED_PLAYBACK;
- public bool IsStopped => TransportState == TransportState.Stopped;
+ public bool IsStopped => TransportState == TransportState.STOPPED;
public Action OnDeviceUnavailable { get; set; }
@@ -494,7 +494,7 @@ namespace Emby.Dlna.PlayTo
cancellationToken: cancellationToken)
.ConfigureAwait(false);
- TransportState = TransportState.Paused;
+ TransportState = TransportState.PAUSED_PLAYBACK;
RestartTimer(true);
}
@@ -527,7 +527,7 @@ namespace Emby.Dlna.PlayTo
if (transportState.HasValue)
{
// If we're not playing anything no need to get additional data
- if (transportState.Value == TransportState.Stopped)
+ if (transportState.Value == TransportState.STOPPED)
{
UpdateMediaInfo(null, transportState.Value);
}
@@ -556,7 +556,7 @@ namespace Emby.Dlna.PlayTo
}
// If we're not playing anything make sure we don't get data more often than necessary to keep the Session alive
- if (transportState.Value == TransportState.Stopped)
+ if (transportState.Value == TransportState.STOPPED)
{
RestartTimerInactive();
}
@@ -1229,7 +1229,7 @@ namespace Emby.Dlna.PlayTo
}
else if (previousMediaInfo == null)
{
- if (state != TransportState.Stopped)
+ if (state != TransportState.STOPPED)
{
OnPlaybackStart(mediaInfo);
}
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 641701e7b..e27a8975b 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -824,7 +824,7 @@ namespace Emby.Dlna.PlayTo
const int Interval = 500;
var currentWait = 0;
- while (_device.TransportState != TransportState.Playing && currentWait < MaxWait)
+ while (_device.TransportState != TransportState.PLAYING && currentWait < MaxWait)
{
await Task.Delay(Interval, cancellationToken).ConfigureAwait(false);
currentWait += Interval;
diff --git a/Emby.Dlna/PlayTo/TransportState.cs b/Emby.Dlna/PlayTo/TransportState.cs
index 2058e9dc7..0d6a78438 100644
--- a/Emby.Dlna/PlayTo/TransportState.cs
+++ b/Emby.Dlna/PlayTo/TransportState.cs
@@ -2,12 +2,15 @@
namespace Emby.Dlna.PlayTo
{
+ /// <summary>
+ /// Core of the AVTransport service. It defines the conceptually top-
+ /// level state of the transport, for example, whether it is playing, recording, etc.
+ /// </summary>
public enum TransportState
{
- Stopped,
- Playing,
- Transitioning,
- PausedPlayback,
- Paused
+ STOPPED,
+ PLAYING,
+ TRANSITIONING,
+ PAUSED_PLAYBACK
}
}
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index eb211050f..961efa48e 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -23,47 +23,61 @@ namespace Emby.Naming.Common
{
VideoFileExtensions = new[]
{
- ".m4v",
+ ".001",
+ ".3g2",
".3gp",
- ".nsv",
- ".ts",
- ".ty",
- ".strm",
- ".rm",
- ".rmvb",
- ".ifo",
- ".mov",
- ".qt",
- ".divx",
- ".xvid",
- ".bivx",
- ".vob",
- ".nrg",
- ".img",
- ".iso",
- ".pva",
- ".wmv",
+ ".amv",
".asf",
".asx",
- ".ogm",
- ".m2v",
".avi",
".bin",
+ ".bivx",
+ ".divx",
+ ".dv",
".dvr-ms",
- ".mpg",
- ".mpeg",
- ".mp4",
+ ".f4v",
+ ".fli",
+ ".flv",
+ ".ifo",
+ ".img",
+ ".iso",
+ ".m2t",
+ ".m2ts",
+ ".m2v",
+ ".m4v",
".mkv",
- ".avc",
- ".vp3",
- ".svq3",
+ ".mk3d",
+ ".mov",
+ ".mp2",
+ ".mp4",
+ ".mpe",
+ ".mpeg",
+ ".mpg",
+ ".mts",
+ ".mxf",
+ ".nrg",
+ ".nsv",
".nuv",
+ ".ogg",
+ ".ogm",
+ ".ogv",
+ ".pva",
+ ".qt",
+ ".rec",
+ ".rm",
+ ".rmvb",
+ ".strm",
+ ".svq3",
+ ".tp",
+ ".ts",
+ ".ty",
".viv",
- ".dv",
- ".fli",
- ".flv",
- ".001",
- ".tp"
+ ".vob",
+ ".vp3",
+ ".webm",
+ ".wmv",
+ ".wtv",
+ ".xvid"
};
VideoFlagDelimiters = new[]
@@ -149,32 +163,20 @@ namespace Emby.Naming.Common
SubtitleFileExtensions = new[]
{
+ ".ass",
+ ".mks",
+ ".sami",
+ ".smi",
".srt",
".ssa",
- ".ass",
- ".sub"
- };
-
- SubtitleFlagDelimiters = new[]
- {
- '.'
- };
-
- SubtitleForcedFlags = new[]
- {
- "foreign",
- "forced"
- };
-
- SubtitleDefaultFlags = new[]
- {
- "default"
+ ".sub",
+ ".vtt",
};
AlbumStackingPrefixes = new[]
{
- "disc",
"cd",
+ "disc",
"disk",
"vol",
"volume"
@@ -182,68 +184,101 @@ namespace Emby.Naming.Common
AudioFileExtensions = new[]
{
- ".nsv",
- ".m4a",
- ".flac",
+ ".669",
+ ".3gp",
+ ".aa",
".aac",
- ".strm",
- ".pls",
- ".rm",
- ".mpa",
- ".wav",
- ".wma",
- ".ogg",
- ".opus",
- ".mp3",
- ".mp2",
- ".mod",
+ ".aax",
+ ".ac3",
+ ".act",
+ ".adp",
+ ".adplug",
+ ".adx",
+ ".afc",
".amf",
- ".669",
+ ".aif",
+ ".aiff",
+ ".alac",
+ ".amr",
+ ".ape",
+ ".ast",
+ ".au",
+ ".awb",
+ ".cda",
+ ".cue",
".dmf",
+ ".dsf",
".dsm",
+ ".dsp",
+ ".dts",
+ ".dvf",
".far",
+ ".flac",
".gdm",
+ ".gsm",
+ ".gym",
+ ".hps",
".imf",
".it",
".m15",
+ ".m4a",
+ ".m4b",
+ ".mac",
".med",
+ ".mka",
+ ".mmf",
+ ".mod",
+ ".mogg",
+ ".mp2",
+ ".mp3",
+ ".mpa",
+ ".mpc",
+ ".mpp",
+ ".mp+",
+ ".msv",
+ ".nmf",
+ ".nsf",
+ ".nsv",
+ ".oga",
+ ".ogg",
".okt",
+ ".opus",
+ ".pls",
+ ".ra",
+ ".rf64",
+ ".rm",
".s3m",
- ".stm",
".sfx",
+ ".shn",
+ ".sid",
+ ".spc",
+ ".stm",
+ ".strm",
".ult",
".uni",
- ".xm",
- ".sid",
- ".ac3",
- ".dts",
- ".cue",
- ".aif",
- ".aiff",
- ".ape",
- ".mac",
- ".mpc",
- ".mp+",
- ".mpp",
- ".shn",
+ ".vox",
+ ".wav",
+ ".wma",
".wv",
- ".nsf",
- ".spc",
- ".gym",
- ".adplug",
- ".adx",
- ".dsp",
- ".adp",
- ".ymf",
- ".ast",
- ".afc",
- ".hps",
+ ".xm",
".xsp",
- ".acc",
- ".m4b",
- ".oga",
- ".dsf",
- ".mka"
+ ".ymf"
+ };
+
+ MediaFlagDelimiters = new[]
+ {
+ '.'
+ };
+
+ MediaForcedFlags = new[]
+ {
+ "foreign",
+ "forced"
+ };
+
+ MediaDefaultFlags = new[]
+ {
+ "default"
};
EpisodeExpressions = new[]
@@ -648,45 +683,6 @@ namespace Emby.Naming.Common
@"^\s*(?<name>[^ ].*?)\s*$"
};
- var extensions = VideoFileExtensions.ToList();
-
- extensions.AddRange(new[]
- {
- ".mkv",
- ".m2t",
- ".m2ts",
- ".img",
- ".iso",
- ".mk3d",
- ".ts",
- ".rmvb",
- ".mov",
- ".avi",
- ".mpg",
- ".mpeg",
- ".wmv",
- ".mp4",
- ".divx",
- ".dvr-ms",
- ".wtv",
- ".ogm",
- ".ogv",
- ".asf",
- ".m4v",
- ".flv",
- ".f4v",
- ".3gp",
- ".webm",
- ".mts",
- ".m2v",
- ".rec",
- ".mxf"
- });
-
- VideoFileExtensions = extensions
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .ToArray();
-
MultipleEpisodeExpressions = new[]
{
@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[eExX](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@@ -718,29 +714,29 @@ namespace Emby.Naming.Common
public string[] AudioFileExtensions { get; set; }
/// <summary>
- /// Gets or sets list of album stacking prefixes.
+ /// Gets or sets list of external media flag delimiters.
/// </summary>
- public string[] AlbumStackingPrefixes { get; set; }
+ public char[] MediaFlagDelimiters { get; set; }
/// <summary>
- /// Gets or sets list of subtitle file extensions.
+ /// Gets or sets list of external media forced flags.
/// </summary>
- public string[] SubtitleFileExtensions { get; set; }
+ public string[] MediaForcedFlags { get; set; }
/// <summary>
- /// Gets or sets list of subtitles flag delimiters.
+ /// Gets or sets list of external media default flags.
/// </summary>
- public char[] SubtitleFlagDelimiters { get; set; }
+ public string[] MediaDefaultFlags { get; set; }
/// <summary>
- /// Gets or sets list of subtitle forced flags.
+ /// Gets or sets list of album stacking prefixes.
/// </summary>
- public string[] SubtitleForcedFlags { get; set; }
+ public string[] AlbumStackingPrefixes { get; set; }
/// <summary>
- /// Gets or sets list of subtitle default flags.
+ /// Gets or sets list of subtitle file extensions.
/// </summary>
- public string[] SubtitleDefaultFlags { get; set; }
+ public string[] SubtitleFileExtensions { get; set; }
/// <summary>
/// Gets or sets list of episode regular expressions.
diff --git a/Emby.Naming/ExternalFiles/ExternalPathParser.cs b/Emby.Naming/ExternalFiles/ExternalPathParser.cs
new file mode 100644
index 000000000..3bde3a1cf
--- /dev/null
+++ b/Emby.Naming/ExternalFiles/ExternalPathParser.cs
@@ -0,0 +1,116 @@
+using System;
+using System.IO;
+using System.Linq;
+using Emby.Naming.Common;
+using Jellyfin.Extensions;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Globalization;
+
+namespace Emby.Naming.ExternalFiles
+{
+ /// <summary>
+ /// External media file parser class.
+ /// </summary>
+ public class ExternalPathParser
+ {
+ private readonly NamingOptions _namingOptions;
+ private readonly DlnaProfileType _type;
+ private readonly ILocalizationManager _localizationManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ExternalPathParser"/> class.
+ /// </summary>
+ /// <param name="localizationManager">The localization manager.</param>
+ /// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param>
+ /// <param name="type">The <see cref="DlnaProfileType"/> of the parsed file.</param>
+ public ExternalPathParser(NamingOptions namingOptions, ILocalizationManager localizationManager, DlnaProfileType type)
+ {
+ _localizationManager = localizationManager;
+ _namingOptions = namingOptions;
+ _type = type;
+ }
+
+ /// <summary>
+ /// Parse filename and extract information.
+ /// </summary>
+ /// <param name="path">Path to file.</param>
+ /// <param name="extraString">Part of the filename only containing the extra information.</param>
+ /// <returns>Returns null or an <see cref="ExternalPathParserResult"/> object if parsing is successful.</returns>
+ public ExternalPathParserResult? ParseFile(string path, string? extraString)
+ {
+ if (path.Length == 0)
+ {
+ return null;
+ }
+
+ var extension = Path.GetExtension(path);
+ if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
+ && !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
+ {
+ return null;
+ }
+
+ var pathInfo = new ExternalPathParserResult(path);
+
+ if (string.IsNullOrEmpty(extraString))
+ {
+ return pathInfo;
+ }
+
+ foreach (var separator in _namingOptions.MediaFlagDelimiters)
+ {
+ var languageString = extraString;
+ var titleString = string.Empty;
+ const int SeparatorLength = 1;
+
+ while (languageString.Length > 0)
+ {
+ int lastSeparator = languageString.LastIndexOf(separator);
+
+ if (lastSeparator == -1)
+ {
+ break;
+ }
+
+ string currentSlice = languageString[lastSeparator..];
+ string currentSliceWithoutSeparator = currentSlice[SeparatorLength..];
+
+ if (_namingOptions.MediaDefaultFlags.Any(s => currentSliceWithoutSeparator.Contains(s, StringComparison.OrdinalIgnoreCase)))
+ {
+ pathInfo.IsDefault = true;
+ extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
+ languageString = languageString[..lastSeparator];
+ continue;
+ }
+
+ if (_namingOptions.MediaForcedFlags.Any(s => currentSliceWithoutSeparator.Contains(s, StringComparison.OrdinalIgnoreCase)))
+ {
+ pathInfo.IsForced = true;
+ extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
+ languageString = languageString[..lastSeparator];
+ continue;
+ }
+
+ // Try to translate to three character code
+ var culture = _localizationManager.FindLanguageInfo(currentSliceWithoutSeparator);
+
+ if (culture != null && pathInfo.Language == null)
+ {
+ pathInfo.Language = culture.ThreeLetterISOLanguageName;
+ extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
+ }
+ else
+ {
+ titleString = currentSlice + titleString;
+ }
+
+ languageString = languageString[..lastSeparator];
+ }
+
+ pathInfo.Title = titleString.Length >= SeparatorLength ? titleString[SeparatorLength..] : null;
+ }
+
+ return pathInfo;
+ }
+ }
+}
diff --git a/Emby.Naming/Subtitles/SubtitleInfo.cs b/Emby.Naming/ExternalFiles/ExternalPathParserResult.cs
index 1fb2e0dc8..1cc773a2e 100644
--- a/Emby.Naming/Subtitles/SubtitleInfo.cs
+++ b/Emby.Naming/ExternalFiles/ExternalPathParserResult.cs
@@ -1,17 +1,17 @@
-namespace Emby.Naming.Subtitles
+namespace Emby.Naming.ExternalFiles
{
/// <summary>
- /// Class holding information about subtitle.
+ /// Class holding information about external files.
/// </summary>
- public class SubtitleInfo
+ public class ExternalPathParserResult
{
/// <summary>
- /// Initializes a new instance of the <see cref="SubtitleInfo"/> class.
+ /// Initializes a new instance of the <see cref="ExternalPathParserResult"/> class.
/// </summary>
/// <param name="path">Path to file.</param>
- /// <param name="isDefault">Is subtitle default.</param>
- /// <param name="isForced">Is subtitle forced.</param>
- public SubtitleInfo(string path, bool isDefault, bool isForced)
+ /// <param name="isDefault">Is default.</param>
+ /// <param name="isForced">Is forced.</param>
+ public ExternalPathParserResult(string path, bool isDefault = false, bool isForced = false)
{
Path = path;
IsDefault = isDefault;
@@ -31,6 +31,12 @@ namespace Emby.Naming.Subtitles
public string? Language { get; set; }
/// <summary>
+ /// Gets or sets the title.
+ /// </summary>
+ /// <value>The title.</value>
+ public string? Title { get; set; }
+
+ /// <summary>
/// Gets or sets a value indicating whether this instance is default.
/// </summary>
/// <value><c>true</c> if this instance is default; otherwise, <c>false</c>.</value>
diff --git a/Emby.Naming/Subtitles/SubtitleParser.cs b/Emby.Naming/Subtitles/SubtitleParser.cs
deleted file mode 100644
index 5809c512a..000000000
--- a/Emby.Naming/Subtitles/SubtitleParser.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using Emby.Naming.Common;
-using Jellyfin.Extensions;
-
-namespace Emby.Naming.Subtitles
-{
- /// <summary>
- /// Subtitle Parser class.
- /// </summary>
- public class SubtitleParser
- {
- private readonly NamingOptions _options;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SubtitleParser"/> class.
- /// </summary>
- /// <param name="options"><see cref="NamingOptions"/> object containing SubtitleFileExtensions, SubtitleDefaultFlags, SubtitleForcedFlags and SubtitleFlagDelimiters.</param>
- public SubtitleParser(NamingOptions options)
- {
- _options = options;
- }
-
- /// <summary>
- /// Parse file to determine if is subtitle and <see cref="SubtitleInfo"/>.
- /// </summary>
- /// <param name="path">Path to file.</param>
- /// <returns>Returns null or <see cref="SubtitleInfo"/> object if parsing is successful.</returns>
- public SubtitleInfo? ParseFile(string path)
- {
- if (path.Length == 0)
- {
- return null;
- }
-
- var extension = Path.GetExtension(path);
- if (!_options.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
- {
- return null;
- }
-
- var flags = GetFlags(path);
- var info = new SubtitleInfo(
- path,
- _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)),
- _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)));
-
- var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparison.OrdinalIgnoreCase)
- && !_options.SubtitleForcedFlags.Contains(i, StringComparison.OrdinalIgnoreCase))
- .ToList();
-
- // Should have a name, language and file extension
- if (parts.Count >= 3)
- {
- info.Language = parts[^2];
- }
-
- return info;
- }
-
- private string[] GetFlags(string path)
- {
- // Note: the tags need be surrounded be either a space ( ), hyphen -, dot . or underscore _.
-
- var file = Path.GetFileName(path);
-
- return file.Split(_options.SubtitleFlagDelimiters, StringSplitOptions.RemoveEmptyEntries);
- }
- }
-}
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index a5cc125ec..886da1390 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -29,7 +29,7 @@
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.2" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.3" />
<PackageReference Include="Mono.Nat" Version="3.0.2" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.3" />
<PackageReference Include="sharpcompress" Version="0.30.1" />
diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
index 34fdfbe8d..e45baedd7 100644
--- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
+++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
@@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.EntryPoints
{
CheckDisposed();
- if (_configurationManager.GetNetworkConfiguration().AutoDiscovery)
+ if (!_configurationManager.GetNetworkConfiguration().AutoDiscovery)
{
return Task.CompletedTask;
}
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 4f8a52f41..399ece7fd 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -704,6 +704,18 @@ namespace Emby.Server.Implementations.IO
return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive));
}
+ /// <inheritdoc />
+ public virtual bool DirectoryExists(string path)
+ {
+ return Directory.Exists(path);
+ }
+
+ /// <inheritdoc />
+ public virtual bool FileExists(string path)
+ {
+ return File.Exists(path);
+ }
+
private EnumerationOptions GetEnumerationOptions(bool recursive)
{
return new EnumerationOptions
diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
index 758986945..57c2f1a5e 100644
--- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
@@ -135,7 +135,7 @@ namespace Emby.Server.Implementations.Images
protected virtual IEnumerable<string> GetStripCollageImagePaths(BaseItem primaryItem, IEnumerable<BaseItem> items)
{
- var useBackdrop = primaryItem is CollectionFolder;
+ var useBackdrop = primaryItem is CollectionFolder || primaryItem is UserView;
return items
.Select(i =>
{
diff --git a/Emby.Server.Implementations/Images/DynamicImageProvider.cs b/Emby.Server.Implementations/Images/DynamicImageProvider.cs
index 575680653..9f9a4902a 100644
--- a/Emby.Server.Implementations/Images/DynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/DynamicImageProvider.cs
@@ -84,16 +84,20 @@ namespace Emby.Server.Implementations.Images
}).GroupBy(x => x.Id)
.Select(x => x.First());
+ List<BaseItem> returnItems;
if (isUsingCollectionStrip)
{
- return items
+ returnItems = items
.Where(i => i.HasImage(ImageType.Primary) || i.HasImage(ImageType.Thumb))
.ToList();
+ returnItems.Shuffle();
+ return returnItems;
}
-
- return items
+ returnItems = items
.Where(i => i.HasImage(ImageType.Primary))
.ToList();
+ returnItems.Shuffle();
+ return returnItems;
}
protected override bool Supports(BaseItem item)
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index d340115a8..a9428ae9b 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -46,6 +46,7 @@ using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
@@ -99,7 +100,7 @@ namespace Emby.Server.Implementations.Library
/// Initializes a new instance of the <see cref="LibraryManager" /> class.
/// </summary>
/// <param name="appHost">The application host.</param>
- /// <param name="logger">The logger.</param>
+ /// <param name="loggerFactory">The logger factory.</param>
/// <param name="taskManager">The task manager.</param>
/// <param name="userManager">The user manager.</param>
/// <param name="configurationManager">The configuration manager.</param>
@@ -115,7 +116,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="namingOptions">The naming options.</param>
public LibraryManager(
IServerApplicationHost appHost,
- ILogger<LibraryManager> logger,
+ ILoggerFactory loggerFactory,
ITaskManager taskManager,
IUserManager userManager,
IServerConfigurationManager configurationManager,
@@ -131,7 +132,7 @@ namespace Emby.Server.Implementations.Library
NamingOptions namingOptions)
{
_appHost = appHost;
- _logger = logger;
+ _logger = loggerFactory.CreateLogger<LibraryManager>();
_taskManager = taskManager;
_userManager = userManager;
_configurationManager = configurationManager;
@@ -146,7 +147,7 @@ namespace Emby.Server.Implementations.Library
_memoryCache = memoryCache;
_namingOptions = namingOptions;
- _extraResolver = new ExtraResolver(namingOptions);
+ _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions);
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
index 9222a9479..3d6b9f3b6 100644
--- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -12,6 +12,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
+using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Resolvers
{
@@ -22,8 +23,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
public abstract class BaseVideoResolver<T> : MediaBrowser.Controller.Resolvers.ItemResolver<T>
where T : Video, new()
{
- protected BaseVideoResolver(NamingOptions namingOptions)
+ private readonly ILogger _logger;
+
+ protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions)
{
+ _logger = logger;
NamingOptions = namingOptions;
}
@@ -156,19 +160,26 @@ namespace Emby.Server.Implementations.Library.Resolvers
}
else
{
- // use disc-utils, both DVDs and BDs use UDF filesystem
- using (var videoFileStream = File.Open(video.Path, FileMode.Open, FileAccess.Read))
- using (UdfReader udfReader = new UdfReader(videoFileStream))
+ try
{
- if (udfReader.DirectoryExists("VIDEO_TS"))
- {
- video.IsoType = IsoType.Dvd;
- }
- else if (udfReader.DirectoryExists("BDMV"))
+ // use disc-utils, both DVDs and BDs use UDF filesystem
+ using (var videoFileStream = File.Open(video.Path, FileMode.Open, FileAccess.Read))
+ using (UdfReader udfReader = new UdfReader(videoFileStream))
{
- video.IsoType = IsoType.BluRay;
+ if (udfReader.DirectoryExists("VIDEO_TS"))
+ {
+ video.IsoType = IsoType.Dvd;
+ }
+ else if (udfReader.DirectoryExists("BDMV"))
+ {
+ video.IsoType = IsoType.BluRay;
+ }
}
}
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error opening UDF/ISO image: {Value}", video.Path ?? video.Name);
+ }
}
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
index 807913b5d..408e640f9 100644
--- a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
@@ -6,6 +6,7 @@ using Emby.Naming.Video;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
+using Microsoft.Extensions.Logging;
using static Emby.Naming.Video.ExtraRuleResolver;
namespace Emby.Server.Implementations.Library.Resolvers
@@ -22,12 +23,13 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <summary>
/// Initializes a new instance of the <see cref="ExtraResolver"/> class.
/// </summary>
+ /// <param name="logger">The logger.</param>
/// <param name="namingOptions">An instance of <see cref="NamingOptions"/>.</param>
- public ExtraResolver(NamingOptions namingOptions)
+ public ExtraResolver(ILogger<ExtraResolver> logger, NamingOptions namingOptions)
{
_namingOptions = namingOptions;
- _trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(namingOptions) };
- _videoResolvers = new IItemResolver[] { new GenericVideoResolver<Video>(namingOptions) };
+ _trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions) };
+ _videoResolvers = new IItemResolver[] { new GenericVideoResolver<Video>(logger, namingOptions) };
}
/// <summary>
diff --git a/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs
index b8554bd51..5e33b402d 100644
--- a/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs
@@ -1,7 +1,8 @@
-#nullable disable
+#nullable disable
using Emby.Naming.Common;
using MediaBrowser.Controller.Entities;
+using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Resolvers
{
@@ -15,9 +16,10 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <summary>
/// Initializes a new instance of the <see cref="GenericVideoResolver{T}"/> class.
/// </summary>
+ /// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param>
- public GenericVideoResolver(NamingOptions namingOptions)
- : base(namingOptions)
+ public GenericVideoResolver(ILogger logger, NamingOptions namingOptions)
+ : base(logger, namingOptions)
{
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index be1460928..140c4272e 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -17,6 +17,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
+using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Resolvers.Movies
{
@@ -40,9 +41,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// Initializes a new instance of the <see cref="MovieResolver"/> class.
/// </summary>
/// <param name="imageProcessor">The image processor.</param>
+ /// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param>
- public MovieResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
- : base(namingOptions)
+ public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions)
+ : base(logger, namingOptions)
{
_imageProcessor = imageProcessor;
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
index be9905647..bfa73af2f 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
@@ -6,6 +6,7 @@ using Emby.Naming.Common;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
+using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Resolvers.TV
{
@@ -17,9 +18,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
/// </summary>
+ /// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param>
- public EpisodeResolver(NamingOptions namingOptions)
- : base(namingOptions)
+ public EpisodeResolver(ILogger<EpisodeResolver> logger, NamingOptions namingOptions)
+ : base(logger, namingOptions)
{
}
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index 7ee8d1040..01a9969b4 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -120,5 +120,7 @@
"Forced": "Vynucené",
"Default": "Výchozí",
"TaskOptimizeDatabaseDescription": "Zmenší databázi a odstraní prázdné místo. Spuštění této úlohy po skenování knihovny či jiných změnách databáze může zlepšit výkon.",
- "TaskOptimizeDatabase": "Optimalizovat databázi"
+ "TaskOptimizeDatabase": "Optimalizovat databázi",
+ "TaskKeyframeExtractorDescription": "Vytahuje klíčové snímky ze souborů videa za účelem vytváření přesnějších seznamů přehrávání HLS. Tento úkol může trvat velmi dlouho.",
+ "TaskKeyframeExtractor": "Vytahovač klíčových snímků"
}
diff --git a/Emby.Server.Implementations/Localization/Core/cy.json b/Emby.Server.Implementations/Localization/Core/cy.json
index 7fc27e18a..981614005 100644
--- a/Emby.Server.Implementations/Localization/Core/cy.json
+++ b/Emby.Server.Implementations/Localization/Core/cy.json
@@ -54,5 +54,57 @@
"Undefined": "Heb ddiffiniad",
"TvShows": "Rhaglenni teledu",
"HeaderLiveTV": "Teledu Byw",
- "User": "Defnyddiwr"
+ "User": "Defnyddiwr",
+ "TaskCleanLogsDescription": "Dileu ffeiliau log sy'n fwy na {0} diwrnod oed.",
+ "TaskCleanLogs": "Glanhau ffolder log",
+ "TaskRefreshLibraryDescription": "Sganio'ch llyfrgell gyfryngau am ffeiliau newydd ac yn adnewyddu metaddata.",
+ "TaskRefreshLibrary": "Sganwich Llyfrgell Cyfryngau",
+ "TaskCleanActivityLogDescription": "Yn dileu cofnodion log gweithgaredd sy'n hŷn na'r oedran a nodwyd.",
+ "TaskCleanActivityLog": "Glanhau Log Gweithgaredd",
+ "SubtitleDownloadFailureFromForItem": "Methodd is-deitlau lawrlwytho o {0} ar gyfer {1}",
+ "NotificationOptionPluginError": "Methodd ategyn",
+ "NotificationOptionAudioPlaybackStopped": "Stopiwyd chwarae sain",
+ "NotificationOptionAudioPlayback": "Dechreuwyd chwarae sain",
+ "MessageServerConfigurationUpdated": "Mae ffurfweddiad gweinydd wedi'i ddiweddaru",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Mae adran ffurfweddu gweinydd {0} wedi'i diweddaru",
+ "FailedLoginAttemptWithUserName": "Cais mewngofnodi wedi methu gan {0}",
+ "ValueHasBeenAddedToLibrary": "{0} wedi'i hychwanegu at eich llyfrgell gyfryngau",
+ "UserStoppedPlayingItemWithValues": "{0} wedi gorffen chwarae {1} ar {2}",
+ "UserStartedPlayingItemWithValues": "{0} yn chwarae {1} ar {2}",
+ "UserPolicyUpdatedWithName": "Polisi defnyddiwr wedi'i newid ar gyfer {0}",
+ "UserPasswordChangedWithName": "Cyfrinair wedi'i newid ar gyfer defnyddiwr {0}",
+ "UserOnlineFromDevice": "Mae {0} ar-lein o {1}",
+ "UserOfflineFromDevice": "Mae {0} wedi datgysylltu o {1}",
+ "UserLockedOutWithName": "Mae defnyddiwr {0} wedi'i gloi allan",
+ "UserDownloadingItemWithValues": "Mae {0} yn lawrlwytho {1}",
+ "UserDeletedWithName": "Defnyddiwr {0} wedi'i ddileu",
+ "UserCreatedWithName": "Defnyddiwr {0} wedi'i greu",
+ "StartupEmbyServerIsLoading": "Gweinydd Jellyfin yn llwytho. Triwch eto mewn ychydig.",
+ "ServerNameNeedsToBeRestarted": "Mae angen ailddechrau {0}",
+ "PluginUpdatedWithName": "{0} wedi'i ddiweddaru",
+ "PluginUninstalledWithName": "{0} wedi'i ddadosod",
+ "PluginInstalledWithName": "{0} wedi'i osod",
+ "NotificationOptionVideoPlaybackStopped": "Stopiwyd chwarae fideo",
+ "NotificationOptionVideoPlayback": "Dechreuwyd chwarae fideo",
+ "NotificationOptionUserLockedOut": "Defnyddiwr wedi'i gloi allan",
+ "NotificationOptionTaskFailed": "Methwyd cyflawni y dasg a drefnwyd",
+ "NotificationOptionServerRestartRequired": "Mae angen ailgychwyn y gweinydd",
+ "NotificationOptionPluginUpdateInstalled": "Diweddariad ategyn wedi'i osod",
+ "NotificationOptionPluginUninstalled": "Ategyn wedi'i ddadosod",
+ "NotificationOptionPluginInstalled": "Ategyn wedi'i osod",
+ "NotificationOptionNewLibraryContent": "Cynnwys newydd ar gael",
+ "NotificationOptionCameraImageUploaded": "Llun camera wedi'i huwchlwytho",
+ "NotificationOptionApplicationUpdateInstalled": "Diweddariad ap wedi'i osod",
+ "NotificationOptionApplicationUpdateAvailable": "Diweddariad ap ar gael",
+ "NewVersionIsAvailable": "Mae fersiwn diweddarach o'r gweinydd Jellyfin ar gael.",
+ "NameInstallFailed": "Gosodiad {0} wedi methu",
+ "MessageApplicationUpdatedTo": "Gweinydd Jellyfin wedi'i ddiweddaru i {0}",
+ "MessageApplicationUpdated": "Gweinydd Jellyfin wedi'i ddiweddaru",
+ "LabelIpAddressValue": "Cyfeiriad IP: {0}",
+ "ItemRemovedWithName": "{0} wedi'i dynnu o'r llyfrgell",
+ "ItemAddedWithName": "{0} wedi'i adio i'r llyfrgell",
+ "HeaderRecordingGroups": "Grwpiau Recordio",
+ "HeaderFavoriteSongs": "Ffefryn Ganeuon",
+ "HeaderFavoriteShows": "Ffefryn Shoeau",
+ "HeaderFavoriteEpisodes": "Ffefryn Rhaglenni"
}
diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json
index 568a8e447..e06f8e6fe 100644
--- a/Emby.Server.Implementations/Localization/Core/en-US.json
+++ b/Emby.Server.Implementations/Localization/Core/en-US.json
@@ -119,5 +119,7 @@
"TaskDownloadMissingSubtitles": "Download missing subtitles",
"TaskDownloadMissingSubtitlesDescription": "Searches the internet for missing subtitles based on metadata configuration.",
"TaskOptimizeDatabase": "Optimize database",
- "TaskOptimizeDatabaseDescription": "Compacts database and truncates free space. Running this task after scanning the library or doing other changes that imply database modifications might improve performance."
+ "TaskOptimizeDatabaseDescription": "Compacts database and truncates free space. Running this task after scanning the library or doing other changes that imply database modifications might improve performance.",
+ "TaskKeyframeExtractor": "Keyframe Extractor",
+ "TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time."
}
diff --git a/Emby.Server.Implementations/Localization/Core/eo.json b/Emby.Server.Implementations/Localization/Core/eo.json
index 8abf7fa66..7d0fca47f 100644
--- a/Emby.Server.Implementations/Localization/Core/eo.json
+++ b/Emby.Server.Implementations/Localization/Core/eo.json
@@ -119,5 +119,7 @@
"HeaderRecordingGroups": "Rikordadaj Grupoj",
"FailedLoginAttemptWithUserName": "Malsukcesa ensaluta provo de {0}",
"CameraImageUploadedFrom": "Nova kamera bildo estis alŝutita de {0}",
- "AuthenticationSucceededWithUserName": "{0} sukcese aŭtentikigis"
+ "AuthenticationSucceededWithUserName": "{0} sukcese aŭtentikigis",
+ "TaskKeyframeExtractorDescription": "Eltiras ĉefkadrojn el videodosieroj por krei pli precizajn HLS-ludlistojn. Ĉi tiu tasko povas funkcii dum longa tempo.",
+ "TaskKeyframeExtractor": "Eltiri Ĉefkadrojn"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json
index 432814dac..80ae16c5c 100644
--- a/Emby.Server.Implementations/Localization/Core/es-MX.json
+++ b/Emby.Server.Implementations/Localization/Core/es-MX.json
@@ -120,5 +120,7 @@
"Forced": "Forzado",
"Default": "Predeterminado",
"TaskOptimizeDatabase": "Optimizar base de datos",
- "TaskOptimizeDatabaseDescription": "Compacta la base de datos y trunca el espacio libre. Puede mejorar el rendimiento si se realiza esta tarea después de escanear la biblioteca o después de realizar otros cambios que impliquen modificar la base de datos."
+ "TaskOptimizeDatabaseDescription": "Compacta la base de datos y trunca el espacio libre. Puede mejorar el rendimiento si se realiza esta tarea después de escanear la biblioteca o después de realizar otros cambios que impliquen modificar la base de datos.",
+ "TaskKeyframeExtractorDescription": "Extrae los cuadros clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar un buen rato.",
+ "TaskKeyframeExtractor": "Extractor de Cuadros Clave"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index f8c69712e..4918f468b 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -120,5 +120,7 @@
"Forced": "Forzado",
"Default": "Predeterminado",
"TaskOptimizeDatabase": "Optimizar la base de datos",
- "TaskOptimizeDatabaseDescription": "Compacta y libera el espacio libre en la base de datos. Ejecutar esta tarea tras escanear la biblioteca o hacer cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento."
+ "TaskOptimizeDatabaseDescription": "Compacta y libera el espacio libre en la base de datos. Ejecutar esta tarea tras escanear la biblioteca o hacer cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento.",
+ "TaskKeyframeExtractorDescription": "Extrae los fotogramas clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar mucho tiempo.",
+ "TaskKeyframeExtractor": "Extractor de Fotogramas Clave"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json
index 6960ff007..7fb560be8 100644
--- a/Emby.Server.Implementations/Localization/Core/fa.json
+++ b/Emby.Server.Implementations/Localization/Core/fa.json
@@ -119,5 +119,8 @@
"TaskCleanActivityLogDescription": "ورودی‌های قدیمی‌تر از سن تنظیم شده در سیاهه فعالیت را حذف می‌کند.",
"TaskCleanActivityLog": "پاکسازی سیاهه فعالیت",
"Undefined": "تعریف نشده",
- "TaskOptimizeDatabase": "بهینه سازی پایگاه داده"
+ "TaskOptimizeDatabase": "بهینه سازی پایگاه داده",
+ "TaskOptimizeDatabaseDescription": "فشرده سازی پایگاه داده و باز کردن فضای آزاد.اجرای این گزینه بعد از اسکن کردن کتابخانه یا تغییرات دیگر که روی پایگاه داده تأثیر میگذارند میتواند کارایی را بهبود ببخشد.",
+ "TaskKeyframeExtractorDescription": "فریم های کلیدی را از فایل های ویدئویی استخراج می کند تا لیست های پخش HLS دقیق تری ایجاد کند. این کار ممکن است برای مدت طولانی اجرا شود.",
+ "TaskKeyframeExtractor": "استخراج کننده فریم کلیدی"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json
index 4a1f4f1d5..435de7363 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/Emby.Server.Implementations/Localization/Core/fi.json
@@ -119,5 +119,7 @@
"TaskCleanActivityLog": "Tyhjennä toimintahistoria",
"Undefined": "Määrittelemätön",
"TaskOptimizeDatabaseDescription": "Tiivistää ja puhdistaa tietokannan. Tämän toiminnon suorittaminen kirjastojen skannauksen tai muiden tietokantaan liittyvien muutoksien jälkeen voi parantaa suorituskykyä.",
- "TaskOptimizeDatabase": "Optimoi tietokanta"
+ "TaskOptimizeDatabase": "Optimoi tietokanta",
+ "TaskKeyframeExtractorDescription": "Purkaa videotiedostojen avainkuvat tarkempien HLS-toistolistojen luomiseksi. Tehtävä saattaa kestää huomattavan pitkään.",
+ "TaskKeyframeExtractor": "Avainkuvien purkain"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index e56ae6071..2a329e74d 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -120,5 +120,7 @@
"Forced": "Forcé",
"Default": "Par défaut",
"TaskOptimizeDatabaseDescription": "Réduit les espaces vides/inutiles et compacte la base de données. Utiliser cette fonction après une mise à jour de la bibliothèque ou toute autre modification de la base de données peut améliorer les performances du serveur.",
- "TaskOptimizeDatabase": "Optimiser la base de données"
+ "TaskOptimizeDatabase": "Optimiser la base de données",
+ "TaskKeyframeExtractorDescription": "Extrait les images clés des fichiers vidéo pour créer des listes de lecture HLS plus précises. Cette tâche peut durer très longtemps.",
+ "TaskKeyframeExtractor": "Extracteur d'image clé"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json
index 85de5925e..781cfcfa2 100644
--- a/Emby.Server.Implementations/Localization/Core/hi.json
+++ b/Emby.Server.Implementations/Localization/Core/hi.json
@@ -61,5 +61,10 @@
"LabelRunningTimeValue": "चलने का समय: {0}",
"ItemAddedWithName": "{0} को लाइब्रेरी में जोड़ा गया",
"Inherit": "इनहेरिट",
- "NotificationOptionVideoPlaybackStopped": "चलचित्र रुका हुआ"
+ "NotificationOptionVideoPlaybackStopped": "चलचित्र रुका हुआ",
+ "PluginUninstalledWithName": "{0} अनइंस्टॉल हुए",
+ "PluginInstalledWithName": "{0} इंस्टॉल हुए",
+ "Plugin": "प्लग-इन",
+ "Playlists": "प्लेलिस्ट",
+ "Photos": "तस्वीरें"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json
index acde84aaf..2da936cff 100644
--- a/Emby.Server.Implementations/Localization/Core/hu.json
+++ b/Emby.Server.Implementations/Localization/Core/hu.json
@@ -120,5 +120,7 @@
"Forced": "Kényszerített",
"Default": "Alapértelmezett",
"TaskOptimizeDatabaseDescription": "Tömöríti az adatbázist és csonkolja a szabad helyet. A feladat futtatása a könyvtár beolvasása után, vagy egyéb, adatbázis-módosítást igénylő változtatások végrehajtása javíthatja a teljesítményt.",
- "TaskOptimizeDatabase": "Adatbázis optimalizálása"
+ "TaskOptimizeDatabase": "Adatbázis optimalizálása",
+ "TaskKeyframeExtractor": "Kulcskockák kibontása",
+ "TaskKeyframeExtractorDescription": "Kulcskockákat bont ki a videofájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat."
}
diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json
index 1b4a18deb..aaaf04712 100644
--- a/Emby.Server.Implementations/Localization/Core/kk.json
+++ b/Emby.Server.Implementations/Localization/Core/kk.json
@@ -120,5 +120,7 @@
"TaskCleanCacheDescription": "Jüiede qajet emes keştelgen faildardy joiady.",
"TaskCleanActivityLogDescription": "Äreket jūrnalyndağy teñşelgen jasynan asqan jazbalary joiady.",
"TaskOptimizeDatabaseDescription": "Derekqordy qysyp, bos oryndy qysqartady. Būl tapsyrmany tasyğyşhanany skanerlegennen keiın nemese derekqorğa meñzeitın basqa özgertuler ıstelgennen keiın oryndau önımdılıktı damytuy mümkın.",
- "TaskOptimizeDatabase": "Derekqordy oñtailandyru"
+ "TaskOptimizeDatabase": "Derekqordy oñtailandyru",
+ "TaskKeyframeExtractorDescription": "Naqtyraq HLS oynatu tızımderın jasau üşın beinefaildardan negızgı kadrlardy şyğarady. Būl tapsyrma ūzaq uaqytqa sozyluy mümkın.",
+ "TaskKeyframeExtractor": "Negızgı kadrlardy şyğaru"
}
diff --git a/Emby.Server.Implementations/Localization/Core/mr.json b/Emby.Server.Implementations/Localization/Core/mr.json
index fdb4171b5..5aad4b0ed 100644
--- a/Emby.Server.Implementations/Localization/Core/mr.json
+++ b/Emby.Server.Implementations/Localization/Core/mr.json
@@ -58,5 +58,47 @@
"Application": "अ‍ॅप्लिकेशन",
"AppDeviceValues": "अ‍ॅप: {0}, यंत्र: {1}",
"Collections": "संग्रह",
- "ChapterNameValue": "धडा {0}"
+ "ChapterNameValue": "धडा {0}",
+ "TaskDownloadMissingSubtitlesDescription": "नसलेल्या उपशिर्षकांचा मेटाडॅटा कॉन्फिग्युरेशनप्रमाणे इन्टरनेटवर शोध घेतो.",
+ "TaskRefreshChannelsDescription": "इन्टरनेट वाहिन्यांची माहिती ताजी करतो.",
+ "TaskUpdatePluginsDescription": "आपोआप अपडेट करण्यासाठी कॉन्फिगर केलेल्या प्लगइनसाठी अपडेट डाउनलोड करून इन्स्टॉल करतो.",
+ "TaskRefreshChannels": "वाहिन्या ताज्या करा",
+ "TaskRefreshPeopleDescription": "आपल्या माध्यम संग्रहातील अभिनेत्यांचा व दिग्दर्शकांचा मेटाडॅटा ताजा करतो.",
+ "TaskRefreshPeople": "लोकांची माहिती ताजी करा",
+ "TaskRefreshLibraryDescription": "माध्यम संग्रह स्कॅन करून नवीन फायली शोधतो व मेटाडॅटा ताजे करतो.",
+ "TaskRefreshLibrary": "माध्यम संग्रह स्कॅन करा",
+ "TaskRefreshChapterImagesDescription": "अध्याय असलेल्या व्हिडियोंसाठी थंबनेल चित्र बनवतो.",
+ "TaskRefreshChapterImages": "अध्याय चित्र काढून घ्या",
+ "TasksMaintenanceCategory": "देखरेख",
+ "ValueHasBeenAddedToLibrary": "{0} हे तुमच्या माध्यम संग्रहात जोडण्यात आले आहे",
+ "UserStoppedPlayingItemWithValues": "{0} यांचं {2} वर {1} पूर्णपणे प्ले करून झालं आहे",
+ "UserStartedPlayingItemWithValues": "{0} हे {2} वर {1} प्ले करत आहे",
+ "UserDownloadingItemWithValues": "{0} हे {1} डाउनलोड करत आहे",
+ "System": "प्रणाली",
+ "Undefined": "अव्याख्यात",
+ "Sync": "सिंक",
+ "ServerNameNeedsToBeRestarted": "{0} याला बंद करून पुन्हा सुरू करायची गरज आहे",
+ "SubtitleDownloadFailureFromForItem": "{0} येथून {1} यासाठी उपशिर्षक डाउनलोड करण्यात अपयश",
+ "ScheduledTaskStartedWithName": "{0} सुरू झाले",
+ "ScheduledTaskFailedWithName": "{0} अपयशी झाले",
+ "ProviderValue": "पुरवणारा: {0}",
+ "PluginUpdatedWithName": "{0} अपडेट केले",
+ "PluginUninstalledWithName": "{0} अनिन्स्टॉल केले",
+ "PluginInstalledWithName": "{0} इन्स्टॉल केले",
+ "NotificationOptionVideoPlaybackStopped": "व्हिडियो प्लेबॅक बंद केले",
+ "NotificationOptionVideoPlayback": "व्हिडियो प्लेबॅक सुरू केले",
+ "NotificationOptionTaskFailed": "अनुसूचित कार्यात अपयश",
+ "NotificationOptionServerRestartRequired": "सर्व्हर बंद करून पुन्हा सुरू करावा लागेल",
+ "NotificationOptionPluginUpdateInstalled": "प्लगइन अपडेट इन्स्टॉल झाले",
+ "NotificationOptionPluginUninstalled": "प्लगइन अनिन्स्टॉल झाले",
+ "NotificationOptionPluginInstalled": "प्लगइन इन्स्टॉल झाले",
+ "NotificationOptionPluginError": "प्लगइनमध्ये अपयश",
+ "NotificationOptionNewLibraryContent": "नवीन सामग्री जोडली गेली",
+ "NotificationOptionInstallationFailed": "इन्स्टॉल करण्यात अपयश",
+ "NotificationOptionAudioPlayback": "ऑडियो प्लेबॅक सुरू झाले",
+ "NotificationOptionAudioPlaybackStopped": "ऑडियो प्लेबॅक बंद झाले",
+ "MixedContent": "मिश्रित सामग्री",
+ "LabelRunningTimeValue": "चालू काल: {0}",
+ "HeaderContinueWatching": "बघणे चालू ठेवा",
+ "Default": "डीफॉल्ट"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json
index 81aa996d9..401e68b2a 100644
--- a/Emby.Server.Implementations/Localization/Core/pr.json
+++ b/Emby.Server.Implementations/Localization/Core/pr.json
@@ -1,7 +1,16 @@
{
- "Books": "Libros",
- "AuthenticationSucceededWithUserName": "{0} autentificado correctamente",
+ "Books": "Scrolls",
+ "AuthenticationSucceededWithUserName": "{0} passed yer trial",
"Artists": "Artistas",
"Songs": "Shantees",
- "Albums": "Ships"
+ "Albums": "Tomes",
+ "Photos": "Paintings",
+ "NotificationOptionUserLockedOut": "Crewmate sent to the brig",
+ "HeaderContinueWatching": "Continue Yer Journey",
+ "Folders": "Chests",
+ "Application": "Captain",
+ "DeviceOnlineWithName": "{0} joined yer crew",
+ "DeviceOfflineWithName": "{0} abandoned ship",
+ "AppDeviceValues": "Captain: {0}, Ship: {1}",
+ "CameraImageUploadedFrom": "Yer looking glass has glimpsed another painting from {0}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index dc3793f1b..dd1e5d0ee 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -120,5 +120,7 @@
"Forced": "Форсир-ые",
"Default": "По умолчанию",
"TaskOptimizeDatabaseDescription": "Сжимает базу данных и вырезает свободные места. Выполнение этой задачи после сканирования библиотеки или внесения других изменений, предполагающих модификации базы данных, может повысить производительность.",
- "TaskOptimizeDatabase": "Оптимизация базы данных"
+ "TaskOptimizeDatabase": "Оптимизация базы данных",
+ "TaskKeyframeExtractorDescription": "Извлекаются ключевые кадры из видеофайлов для создания более точных списков плей-листов HLS. Эта задача может выполняться в течение длительного времени.",
+ "TaskKeyframeExtractor": "Извлечение ключевых кадров"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index 5d05361b0..10c6db63b 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/Emby.Server.Implementations/Localization/Core/sv.json
@@ -120,5 +120,7 @@
"Forced": "Tvingad",
"Default": "Standard",
"TaskOptimizeDatabase": "Optimera databasen",
- "TaskOptimizeDatabaseDescription": "Komprimerar databasen och trunkerar ledigt utrymme. Prestandan kan förbättras genom att köra denna task efter att du har skannat biblioteket eller gjort andra förändringar som indikerar att databasen har modifierats."
+ "TaskOptimizeDatabaseDescription": "Komprimerar databasen och trunkerar ledigt utrymme. Prestandan kan förbättras genom att köra denna task efter att du har skannat biblioteket eller gjort andra förändringar som indikerar att databasen har modifierats.",
+ "TaskKeyframeExtractorDescription": "Expoterar nyckelram från video filer för att skapa mer exakta HLS-spellistor. Denna uppgift kan pågå under lång tid.",
+ "TaskKeyframeExtractor": "Nyckelram Extraktor"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json
index 98d763fcd..5548a74d2 100644
--- a/Emby.Server.Implementations/Localization/Core/ta.json
+++ b/Emby.Server.Implementations/Localization/Core/ta.json
@@ -119,5 +119,7 @@
"Forced": "கட்டாயப்படுத்தப்பட்டது",
"Default": "இயல்புநிலை",
"TaskOptimizeDatabaseDescription": "தரவுத்தளத்தை சுருக்கி, இலவச இடத்தை குறைக்கிறது. நூலகத்தை ஸ்கேன் செய்தபின் அல்லது தரவுத்தள மாற்றங்களைக் குறிக்கும் பிற மாற்றங்களைச் செய்தபின் இந்த பணியை இயக்குவது செயல்திறனை மேம்படுத்தக்கூடும்.",
- "TaskOptimizeDatabase": "தரவுத்தளத்தை மேம்படுத்தவும்"
+ "TaskOptimizeDatabase": "தரவுத்தளத்தை மேம்படுத்தவும்",
+ "TaskKeyframeExtractorDescription": "மிகவும் துல்லியமான HLS பிளேலிஸ்ட்களை உருவாக்க வீடியோ கோப்புகளிலிருந்து கீஃப்ரேம்களைப் பிரித்தெடுக்கிறது. இந்த பணி நீண்ட காலமாக இருக்கலாம்.",
+ "TaskKeyframeExtractor": "கீஃப்ரேம் எக்ஸ்ட்ராக்டர்"
}
diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json
index d0e08d8ee..a9268c7d5 100644
--- a/Emby.Server.Implementations/Localization/Core/vi.json
+++ b/Emby.Server.Implementations/Localization/Core/vi.json
@@ -119,5 +119,7 @@
"Forced": "Bắt Buộc",
"Default": "Mặc Định",
"TaskOptimizeDatabaseDescription": "Thu gọn cơ sở dữ liệu và cắt bớt dung lượng trống. Chạy tác vụ này sau khi quét thư viện hoặc thực hiện các thay đổi khác ngụ ý sửa đổi cơ sở dữ liệu có thể cải thiện hiệu suất.",
- "TaskOptimizeDatabase": "Tối ưu hóa cơ sở dữ liệu"
+ "TaskOptimizeDatabase": "Tối ưu hóa cơ sở dữ liệu",
+ "TaskKeyframeExtractor": "Trích Xuất Khung Hình",
+ "TaskKeyframeExtractorDescription": "Trích xuất khung hình chính từ các tệp video để tạo danh sách phát HLS chính xác hơn. Tác vụ này có thể chạy trong một thời gian dài."
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index ac4eb644b..23d2819c3 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json
@@ -120,5 +120,7 @@
"Forced": "强制的",
"Default": "默认",
"TaskOptimizeDatabaseDescription": "压缩数据库并优化可用空间,在扫描库或执行其他数据库修改后运行此任务可能会提高性能。",
- "TaskOptimizeDatabase": "优化数据库"
+ "TaskOptimizeDatabase": "优化数据库",
+ "TaskKeyframeExtractorDescription": "从视频文件中提取关键帧以创建更准确的HLS播放列表。这项任务可能需要很长时间。",
+ "TaskKeyframeExtractor": "关键帧提取器"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json
index 1cc97bc27..ac74da67d 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-HK.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json
@@ -103,7 +103,7 @@
"TaskCleanTranscodeDescription": "刪除超過一天的轉碼文件。",
"TaskCleanTranscode": "清理轉碼目錄",
"TaskUpdatePluginsDescription": "下載並安裝配置為自動更新的插件的更新。",
- "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的metadata。",
+ "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的元數據。",
"TaskCleanLogsDescription": "刪除超過{0}天的日誌文件。",
"TaskCleanLogs": "清理日誌目錄",
"TaskRefreshLibrary": "掃描媒體庫",
diff --git a/Emby.Server.Implementations/Localization/Ratings/au.csv b/Emby.Server.Implementations/Localization/Ratings/au.csv
index 940375e26..11f4ed94c 100644
--- a/Emby.Server.Implementations/Localization/Ratings/au.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/au.csv
@@ -2,7 +2,6 @@ AU-G,1
AU-PG,5
AU-M,6
AU-MA15+,7
-AU-M15+,8
AU-R18+,9
AU-X18+,10
AU-RC,11
diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs
index fd3fc31c9..21795c8f8 100644
--- a/Emby.Server.Implementations/Net/SocketFactory.cs
+++ b/Emby.Server.Implementations/Net/SocketFactory.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -63,18 +61,13 @@ namespace Emby.Server.Implementations.Net
}
/// <inheritdoc />
- public ISocket CreateUdpMulticastSocket(string ipAddress, int multicastTimeToLive, int localPort)
+ public ISocket CreateUdpMulticastSocket(IPAddress ipAddress, int multicastTimeToLive, int localPort)
{
if (ipAddress == null)
{
throw new ArgumentNullException(nameof(ipAddress));
}
- if (ipAddress.Length == 0)
- {
- throw new ArgumentException("ipAddress cannot be an empty string.", nameof(ipAddress));
- }
-
if (multicastTimeToLive <= 0)
{
throw new ArgumentException("multicastTimeToLive cannot be zero or less.", nameof(multicastTimeToLive));
@@ -87,14 +80,7 @@ namespace Emby.Server.Implementations.Net
var retVal = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
- try
- {
- // not supported on all platforms. throws on ubuntu with .net core 2.0
- retVal.ExclusiveAddressUse = false;
- }
- catch (SocketException)
- {
- }
+ retVal.ExclusiveAddressUse = false;
try
{
@@ -114,7 +100,7 @@ namespace Emby.Server.Implementations.Net
var localIp = IPAddress.Any;
- retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse(ipAddress), localIp));
+ retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(ipAddress, localIp));
retVal.MulticastLoopback = true;
return new UdpSocket(retVal, localPort, localIp);
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 5d16f8353..277fdf87d 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -424,9 +424,14 @@ namespace Emby.Server.Implementations.Session
var nowPlayingQueue = info.NowPlayingQueue;
- if (nowPlayingQueue != null)
+ if (nowPlayingQueue?.Length > 0)
{
session.NowPlayingQueue = nowPlayingQueue;
+
+ var itemIds = nowPlayingQueue.Select(queue => queue.Id).ToArray();
+ session.NowPlayingQueueFullItems = _dtoService.GetBaseItemDtos(
+ _libraryManager.GetItemList(new InternalItemsQuery { ItemIds = itemIds }),
+ new DtoOptions(true));
}
}
diff --git a/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs b/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs
new file mode 100644
index 000000000..e39280a10
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs
@@ -0,0 +1,50 @@
+using System;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ /// <summary>
+ /// Class IndexNumberComparer.
+ /// </summary>
+ public class IndexNumberComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.IndexNumber;
+
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem? x, BaseItem? y)
+ {
+ if (x == null)
+ {
+ throw new ArgumentNullException(nameof(x));
+ }
+
+ if (y == null)
+ {
+ throw new ArgumentNullException(nameof(y));
+ }
+
+ if (!x.IndexNumber.HasValue)
+ {
+ return -1;
+ }
+
+ if (!y.IndexNumber.HasValue)
+ {
+ return 1;
+ }
+
+ return x.IndexNumber.Value.CompareTo(y.IndexNumber.Value);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs b/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs
new file mode 100644
index 000000000..ffc4e0cad
--- /dev/null
+++ b/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs
@@ -0,0 +1,50 @@
+using System;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Sorting
+{
+ /// <summary>
+ /// Class ParentIndexNumberComparer.
+ /// </summary>
+ public class ParentIndexNumberComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.ParentIndexNumber;
+
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem? x, BaseItem? y)
+ {
+ if (x == null)
+ {
+ throw new ArgumentNullException(nameof(x));
+ }
+
+ if (y == null)
+ {
+ throw new ArgumentNullException(nameof(y));
+ }
+
+ if (!x.ParentIndexNumber.HasValue)
+ {
+ return -1;
+ }
+
+ if (!y.ParentIndexNumber.HasValue)
+ {
+ return 1;
+ }
+
+ return x.ParentIndexNumber.Value.CompareTo(y.ParentIndexNumber.Value);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index 3f2d6a55c..727b9d4b5 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -126,7 +126,8 @@ namespace Emby.Server.Implementations.TV
parentsFolders.ToList())
.Cast<Episode>()
.Where(episode => !string.IsNullOrEmpty(episode.SeriesPresentationUniqueKey))
- .Select(GetUniqueSeriesKey);
+ .Select(GetUniqueSeriesKey)
+ .ToList();
// Avoid implicitly captured closure
var episodes = GetNextUpEpisodes(request, user, items, options);
@@ -134,13 +135,21 @@ namespace Emby.Server.Implementations.TV
return GetResult(episodes, request);
}
- public IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IEnumerable<string> seriesKeys, DtoOptions dtoOptions)
+ public IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IReadOnlyList<string> seriesKeys, DtoOptions dtoOptions)
{
// Avoid implicitly captured closure
var currentUser = user;
var allNextUp = seriesKeys
- .Select(i => GetNextUp(i, currentUser, dtoOptions, request.Rewatching));
+ .Select(i => GetNextUp(i, currentUser, dtoOptions, false));
+
+ if (request.EnableRewatching)
+ {
+ allNextUp = allNextUp.Concat(
+ seriesKeys.Select(i => GetNextUp(i, currentUser, dtoOptions, true))
+ )
+ .OrderByDescending(i => i.Item1);
+ }
// If viewing all next up for all series, remove first episodes
// But if that returns empty, keep those first episodes (avoid completely empty view)
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 0b2604640..27eb22339 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -126,9 +126,11 @@ namespace Jellyfin.Api.Controllers
HomeSectionType.SmallLibraryTiles,
HomeSectionType.Resume,
HomeSectionType.ResumeAudio,
+ HomeSectionType.ResumeBook,
HomeSectionType.LiveTv,
HomeSectionType.NextUp,
- HomeSectionType.LatestMedia, HomeSectionType.None,
+ HomeSectionType.LatestMedia,
+ HomeSectionType.None,
};
if (!Guid.TryParse(displayPreferencesId, out var itemId))
@@ -182,7 +184,7 @@ namespace Jellyfin.Api.Controllers
var order = int.Parse(key.AsSpan().Slice("homesection".Length));
if (!Enum.TryParse<HomeSectionType>(displayPreferences.CustomPrefs[key], true, out var type))
{
- type = order < 7 ? defaults[order] : HomeSectionType.None;
+ type = order < 8 ? defaults[order] : HomeSectionType.None;
}
displayPreferences.CustomPrefs.Remove(key);
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index f2fdeeea5..f8e8d975c 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -1599,7 +1599,6 @@ namespace Jellyfin.Api.Controllers
state.BaseRequest.BreakOnNonKeyFrames = false;
}
- var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions);
var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
@@ -1608,12 +1607,15 @@ namespace Jellyfin.Api.Controllers
var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
var outputTsArg = outputPrefix + "%d" + outputExtension;
- var segmentFormat = outputExtension.TrimStart('.');
- if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
+ var segmentFormat = string.Empty;
+ var segmentContainer = outputExtension.TrimStart('.');
+ var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions, segmentContainer);
+
+ if (string.Equals(segmentContainer, "ts", StringComparison.OrdinalIgnoreCase))
{
segmentFormat = "mpegts";
}
- else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase))
+ else if (string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase))
{
var outputFmp4HeaderArg = OperatingSystem.IsWindows() switch
{
@@ -1627,7 +1629,8 @@ namespace Jellyfin.Api.Controllers
}
else
{
- _logger.LogError("Invalid HLS segment container: {SegmentFormat}", segmentFormat);
+ _logger.LogError("Invalid HLS segment container: {SegmentContainer}, default to mpegts", segmentContainer);
+ segmentFormat = "mpegts";
}
var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128
@@ -1647,7 +1650,7 @@ namespace Jellyfin.Api.Controllers
CultureInfo.InvariantCulture,
"{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9}{10} -hls_segment_filename \"{12}\" -hls_playlist_type {11} -hls_list_size 0 -y \"{13}\"",
inputModifier,
- _encodingHelper.GetInputArgument(state, _encodingOptions),
+ _encodingHelper.GetInputArgument(state, _encodingOptions, segmentContainer),
threads,
mapArgs,
GetVideoArguments(state, startNumber, isEventPlaylist),
@@ -1944,7 +1947,7 @@ namespace Jellyfin.Api.Controllers
return Task.CompletedTask;
});
- return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath), false, HttpContext);
+ return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath));
}
private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs
index 7325dca0a..78634f0bf 100644
--- a/Jellyfin.Api/Controllers/HlsSegmentController.cs
+++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs
@@ -69,7 +69,7 @@ namespace Jellyfin.Api.Controllers
return BadRequest("Invalid segment.");
}
- return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file), false, HttpContext);
+ return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file));
}
/// <summary>
@@ -186,7 +186,7 @@ namespace Jellyfin.Api.Controllers
return Task.CompletedTask;
});
- return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path), false, HttpContext);
+ return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path));
}
}
}
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index aafffc2a1..05d80ba35 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -570,8 +570,7 @@ namespace Jellyfin.Api.Controllers
blur,
backgroundColor,
foregroundLayer,
- item,
- Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ item)
.ConfigureAwait(false);
}
@@ -654,8 +653,7 @@ namespace Jellyfin.Api.Controllers
blur,
backgroundColor,
foregroundLayer,
- item,
- Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ item)
.ConfigureAwait(false);
}
@@ -738,8 +736,7 @@ namespace Jellyfin.Api.Controllers
blur,
backgroundColor,
foregroundLayer,
- item,
- Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ item)
.ConfigureAwait(false);
}
@@ -822,8 +819,7 @@ namespace Jellyfin.Api.Controllers
blur,
backgroundColor,
foregroundLayer,
- item,
- Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ item)
.ConfigureAwait(false);
}
@@ -906,8 +902,7 @@ namespace Jellyfin.Api.Controllers
blur,
backgroundColor,
foregroundLayer,
- item,
- Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ item)
.ConfigureAwait(false);
}
@@ -990,8 +985,7 @@ namespace Jellyfin.Api.Controllers
blur,
backgroundColor,
foregroundLayer,
- item,
- Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ item)
.ConfigureAwait(false);
}
@@ -1074,8 +1068,7 @@ namespace Jellyfin.Api.Controllers
blur,
backgroundColor,
foregroundLayer,
- item,
- Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ item)
.ConfigureAwait(false);
}
@@ -1158,8 +1151,7 @@ namespace Jellyfin.Api.Controllers
blur,
backgroundColor,
foregroundLayer,
- item,
- Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ item)
.ConfigureAwait(false);
}
@@ -1242,8 +1234,7 @@ namespace Jellyfin.Api.Controllers
blur,
backgroundColor,
foregroundLayer,
- item,
- Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ item)
.ConfigureAwait(false);
}
@@ -1326,8 +1317,7 @@ namespace Jellyfin.Api.Controllers
blur,
backgroundColor,
foregroundLayer,
- item,
- Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ item)
.ConfigureAwait(false);
}
@@ -1410,8 +1400,7 @@ namespace Jellyfin.Api.Controllers
blur,
backgroundColor,
foregroundLayer,
- item,
- Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ item)
.ConfigureAwait(false);
}
@@ -1494,8 +1483,7 @@ namespace Jellyfin.Api.Controllers
blur,
backgroundColor,
foregroundLayer,
- item,
- Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ item)
.ConfigureAwait(false);
}
@@ -1596,7 +1584,6 @@ namespace Jellyfin.Api.Controllers
backgroundColor,
foregroundLayer,
null,
- Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase),
info)
.ConfigureAwait(false);
}
@@ -1698,7 +1685,6 @@ namespace Jellyfin.Api.Controllers
backgroundColor,
foregroundLayer,
null,
- Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase),
info)
.ConfigureAwait(false);
}
@@ -1784,8 +1770,7 @@ namespace Jellyfin.Api.Controllers
return await GetImageResult(
options,
cacheDuration,
- ImmutableDictionary<string, string>.Empty,
- Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ ImmutableDictionary<string, string>.Empty)
.ConfigureAwait(false);
}
@@ -1907,7 +1892,6 @@ namespace Jellyfin.Api.Controllers
string? backgroundColor,
string? foregroundLayer,
BaseItem? item,
- bool isHeadRequest,
ItemImageInfo? imageInfo = null)
{
if (percentPlayed.HasValue)
@@ -1988,8 +1972,7 @@ namespace Jellyfin.Api.Controllers
return await GetImageResult(
options,
cacheDuration,
- responseHeaders,
- isHeadRequest).ConfigureAwait(false);
+ responseHeaders).ConfigureAwait(false);
}
private ImageFormat[] GetOutputFormats(ImageFormat? format)
@@ -2068,8 +2051,7 @@ namespace Jellyfin.Api.Controllers
private async Task<ActionResult> GetImageResult(
ImageProcessingOptions imageProcessingOptions,
TimeSpan? cacheDuration,
- IDictionary<string, string> headers,
- bool isHeadRequest)
+ IDictionary<string, string> headers)
{
var (imagePath, imageContentType, dateImageModified) = await _imageProcessor.ProcessImage(imageProcessingOptions).ConfigureAwait(false);
@@ -2120,12 +2102,6 @@ namespace Jellyfin.Api.Controllers
}
}
- // if the request is a head request, return a NoContent result with the same headers as it would with a GET request
- if (isHeadRequest)
- {
- return NoContent();
- }
-
return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain);
}
}
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index b41df1abb..b227dba2d 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -7,7 +7,6 @@ using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
-using Jellyfin.Api.Models.PluginDtos;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
@@ -44,61 +43,6 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
- /// Get plugin security info.
- /// </summary>
- /// <response code="200">Plugin security info returned.</response>
- /// <returns>Plugin security info.</returns>
- [Obsolete("This endpoint should not be used.")]
- [HttpGet("SecurityInfo")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public static ActionResult<PluginSecurityInfo> GetPluginSecurityInfo()
- {
- return new PluginSecurityInfo
- {
- IsMbSupporter = true,
- SupporterKey = "IAmTotallyLegit"
- };
- }
-
- /// <summary>
- /// Gets registration status for a feature.
- /// </summary>
- /// <param name="name">Feature name.</param>
- /// <response code="200">Registration status returned.</response>
- /// <returns>Mb registration record.</returns>
- [Obsolete("This endpoint should not be used.")]
- [HttpPost("RegistrationRecords/{name}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public static ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name)
- {
- return new MBRegistrationRecord
- {
- IsRegistered = true,
- RegChecked = true,
- TrialVersion = false,
- IsValid = true,
- RegError = false
- };
- }
-
- /// <summary>
- /// Gets registration status for a feature.
- /// </summary>
- /// <param name="name">Feature name.</param>
- /// <response code="501">Not implemented.</response>
- /// <returns>Not Implemented.</returns>
- /// <exception cref="NotImplementedException">This endpoint is not implemented.</exception>
- [Obsolete("Paid plugins are not supported")]
- [HttpGet("Registrations/{name}")]
- [ProducesResponseType(StatusCodes.Status501NotImplemented)]
- public static ActionResult GetRegistration([FromRoute, Required] string name)
- {
- // TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins,
- // delete all these registration endpoints. They are only kept for compatibility.
- throw new NotImplementedException();
- }
-
- /// <summary>
/// Gets a list of currently installed plugins.
/// </summary>
/// <response code="200">Installed plugins returned.</response>
@@ -317,20 +261,5 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
-
- /// <summary>
- /// Updates plugin security info.
- /// </summary>
- /// <param name="pluginSecurityInfo">Plugin security info.</param>
- /// <response code="204">Plugin security info updated.</response>
- /// <returns>An <see cref="NoContentResult"/>.</returns>
- [Obsolete("This endpoint should not be used.")]
- [HttpPost("SecurityInfo")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult UpdatePluginSecurityInfo([FromBody, Required] PluginSecurityInfo pluginSecurityInfo)
- {
- return NoContent();
- }
}
}
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 2572ba63b..179a53fd5 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -68,7 +68,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="nextUpDateCutoff">Optional. Starting date of shows to show in Next Up section.</param>
/// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
/// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param>
- /// <param name="rewatching">Whether to get a rewatching next up instead of standard next up.</param>
+ /// <param name="enableRewatching">Whether to include watched episode in next up results.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
[HttpGet("NextUp")]
[ProducesResponseType(StatusCodes.Status200OK)]
@@ -86,7 +86,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] DateTime? nextUpDateCutoff,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool disableFirstEpisode = false,
- [FromQuery] bool rewatching = false)
+ [FromQuery] bool enableRewatching = false)
{
var options = new DtoOptions { Fields = fields }
.AddClientFields(Request)
@@ -103,7 +103,7 @@ namespace Jellyfin.Api.Controllers
EnableTotalRecordCount = enableTotalRecordCount,
DisableFirstEpisode = disableFirstEpisode,
NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue,
- Rewatching = rewatching
+ EnableRewatching = enableRewatching
},
options);
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 74b4f084f..62c05331e 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -465,7 +465,7 @@ namespace Jellyfin.Api.Controllers
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager);
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
- return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, HttpContext).ConfigureAwait(false);
+ return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, httpClient, HttpContext).ConfigureAwait(false);
}
if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
@@ -494,9 +494,7 @@ namespace Jellyfin.Api.Controllers
return FileStreamResponseHelpers.GetStaticFileResult(
state.MediaPath,
- contentType,
- isHeadRequest,
- HttpContext);
+ contentType);
}
// Need to start ffmpeg (because media can't be returned directly)
diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs
index bec961dad..27497cd59 100644
--- a/Jellyfin.Api/Helpers/AudioHelper.cs
+++ b/Jellyfin.Api/Helpers/AudioHelper.cs
@@ -138,7 +138,7 @@ namespace Jellyfin.Api.Helpers
StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
- return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, _httpContextAccessor.HttpContext).ConfigureAwait(false);
+ return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, httpClient, _httpContextAccessor.HttpContext).ConfigureAwait(false);
}
if (streamingRequest.Static && state.InputProtocol != MediaProtocol.File)
@@ -167,9 +167,7 @@ namespace Jellyfin.Api.Helpers
return FileStreamResponseHelpers.GetStaticFileResult(
state.MediaPath,
- contentType,
- isHeadRequest,
- _httpContextAccessor.HttpContext);
+ contentType);
}
// Need to start ffmpeg (because media can't be returned directly)
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index 6385b62c9..5bdd3fe2e 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -22,14 +22,12 @@ namespace Jellyfin.Api.Helpers
/// Returns a static file from a remote source.
/// </summary>
/// <param name="state">The current <see cref="StreamState"/>.</param>
- /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
/// <param name="httpClient">The <see cref="HttpClient"/> making the remote request.</param>
/// <param name="httpContext">The current http context.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A <see cref="Task{ActionResult}"/> containing the API response.</returns>
public static async Task<ActionResult> GetStaticRemoteStreamResult(
StreamState state,
- bool isHeadRequest,
HttpClient httpClient,
HttpContext httpContext,
CancellationToken cancellationToken = default)
@@ -45,12 +43,6 @@ namespace Jellyfin.Api.Helpers
httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
- if (isHeadRequest)
- {
- httpContext.Response.Headers[HeaderNames.ContentType] = contentType;
- return new OkResult();
- }
-
return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType);
}
@@ -59,23 +51,11 @@ namespace Jellyfin.Api.Helpers
/// </summary>
/// <param name="path">The path to the file.</param>
/// <param name="contentType">The content type of the file.</param>
- /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
- /// <param name="httpContext">The current http context.</param>
/// <returns>An <see cref="ActionResult"/> the file.</returns>
public static ActionResult GetStaticFileResult(
string path,
- string contentType,
- bool isHeadRequest,
- HttpContext httpContext)
+ string contentType)
{
- httpContext.Response.ContentType = contentType;
-
- // if the request is a head request, return an OkResult (200) with the same headers as it would with a GET request
- if (isHeadRequest)
- {
- return new OkResult();
- }
-
return new PhysicalFileResult(path, contentType) { EnableRangeProcessing = true };
}
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 56a54fb1d..c8762b7c5 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -18,6 +18,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
@@ -42,6 +43,8 @@ namespace Jellyfin.Api.Helpers
/// </summary>
private static readonly Dictionary<string, SemaphoreSlim> _transcodingLocks = new Dictionary<string, SemaphoreSlim>();
+ private readonly IAttachmentExtractor _attachmentExtractor;
+ private readonly IApplicationPaths _appPaths;
private readonly IAuthorizationContext _authorizationContext;
private readonly EncodingHelper _encodingHelper;
private readonly IFileSystem _fileSystem;
@@ -55,6 +58,8 @@ namespace Jellyfin.Api.Helpers
/// <summary>
/// Initializes a new instance of the <see cref="TranscodingJobHelper"/> class.
/// </summary>
+ /// <param name="attachmentExtractor">Instance of the <see cref="IAttachmentExtractor"/> interface.</param>
+ /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{TranscodingJobHelpers}"/> interface.</param>
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
@@ -65,6 +70,8 @@ namespace Jellyfin.Api.Helpers
/// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
public TranscodingJobHelper(
+ IAttachmentExtractor attachmentExtractor,
+ IApplicationPaths appPaths,
ILogger<TranscodingJobHelper> logger,
IMediaSourceManager mediaSourceManager,
IFileSystem fileSystem,
@@ -75,6 +82,8 @@ namespace Jellyfin.Api.Helpers
EncodingHelper encodingHelper,
ILoggerFactory loggerFactory)
{
+ _attachmentExtractor = attachmentExtractor;
+ _appPaths = appPaths;
_logger = logger;
_mediaSourceManager = mediaSourceManager;
_fileSystem = fileSystem;
@@ -449,9 +458,12 @@ namespace Jellyfin.Api.Helpers
var audioCodec = state.ActualOutputAudioCodec;
var videoCodec = state.ActualOutputVideoCodec;
var hardwareAccelerationTypeString = _serverConfigurationManager.GetEncodingOptions().HardwareAccelerationType;
- HardwareEncodingType? hardwareAccelerationType = string.IsNullOrEmpty(hardwareAccelerationTypeString)
- ? null
- : (HardwareEncodingType)Enum.Parse(typeof(HardwareEncodingType), hardwareAccelerationTypeString, true);
+ HardwareEncodingType? hardwareAccelerationType = null;
+ if (!string.IsNullOrEmpty(hardwareAccelerationTypeString)
+ && Enum.TryParse<HardwareEncodingType>(hardwareAccelerationTypeString, out var parsedHardwareAccelerationType))
+ {
+ hardwareAccelerationType = parsedHardwareAccelerationType;
+ }
_sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
{
@@ -513,6 +525,13 @@ namespace Jellyfin.Api.Helpers
throw new ArgumentException("FFmpeg path not set.");
}
+ // If subtitles get burned in fonts may need to be extracted from the media file
+ if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+ {
+ var attachmentPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id);
+ await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, CancellationToken.None).ConfigureAwait(false);
+ }
+
var process = new Process
{
StartInfo = new ProcessStartInfo
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index c5b240e92..b0b74e7ed 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -17,7 +17,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.2" />
+ <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.3" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.2.3" />
diff --git a/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs b/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs
deleted file mode 100644
index 7f1255f4b..000000000
--- a/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using System;
-
-namespace Jellyfin.Api.Models.PluginDtos
-{
- /// <summary>
- /// MB Registration Record.
- /// </summary>
- public class MBRegistrationRecord
- {
- /// <summary>
- /// Gets or sets expiration date.
- /// </summary>
- public DateTime ExpirationDate { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether is registered.
- /// </summary>
- public bool IsRegistered { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether reg checked.
- /// </summary>
- public bool RegChecked { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether reg error.
- /// </summary>
- public bool RegError { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether trial version.
- /// </summary>
- public bool TrialVersion { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether is valid.
- /// </summary>
- public bool IsValid { get; set; }
- }
-}
diff --git a/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs b/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs
deleted file mode 100644
index a90398425..000000000
--- a/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-namespace Jellyfin.Api.Models.PluginDtos
-{
- /// <summary>
- /// Plugin security info.
- /// </summary>
- public class PluginSecurityInfo
- {
- /// <summary>
- /// Gets or sets the supporter key.
- /// </summary>
- public string? SupporterKey { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether is mb supporter.
- /// </summary>
- public bool IsMbSupporter { get; set; }
- }
-}
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index b7dab82af..2a75c2bf4 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -27,13 +27,13 @@
<ItemGroup>
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.2" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.2" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.2">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.3" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.3" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.2">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 6743a24aa..fadacc0b5 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -37,10 +37,10 @@
<PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.2" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.2" />
- <PackageReference Include="prometheus-net" Version="5.0.2" />
- <PackageReference Include="prometheus-net.AspNetCore" Version="5.0.2" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.3" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.3" />
+ <PackageReference Include="prometheus-net" Version="6.0.0" />
+ <PackageReference Include="prometheus-net.AspNetCore" Version="6.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.3.0" />
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
index 21f153623..afd7aee5d 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
@@ -58,13 +58,18 @@ namespace Jellyfin.Server.Migrations.Routines
foreach (var row in authenticatedDevices)
{
+ var dateCreatedStr = row[9].ToString();
+ _ = DateTime.TryParse(dateCreatedStr, out var dateCreated);
+ var dateLastActivityStr = row[10].ToString();
+ _ = DateTime.TryParse(dateLastActivityStr, out var dateLastActivity);
+
if (row[6].IsDbNull())
{
dbContext.ApiKeys.Add(new ApiKey(row[3].ToString())
{
AccessToken = row[1].ToString(),
- DateCreated = row[9].ToDateTime(),
- DateLastActivity = row[10].ToDateTime()
+ DateCreated = dateCreated,
+ DateLastActivity = dateLastActivity
});
}
else
@@ -78,8 +83,8 @@ namespace Jellyfin.Server.Migrations.Routines
{
AccessToken = row[1].ToString(),
IsActive = row[8].ToBool(),
- DateCreated = row[9].ToDateTime(),
- DateLastActivity = row[10].ToDateTime()
+ DateCreated = dateCreated,
+ DateLastActivity = dateLastActivity
});
}
}
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 74adf38a9..d993a15a9 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -887,7 +887,7 @@ namespace MediaBrowser.Controller.Entities
return Name;
}
- public string GetInternalMetadataPath()
+ public virtual string GetInternalMetadataPath()
{
var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath;
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 7955adab4..f9450ccb3 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -850,6 +850,18 @@ namespace MediaBrowser.Controller.Entities
return true;
}
+ if (query.HasThemeSong.HasValue)
+ {
+ Logger.LogDebug("Query requires post-filtering due to HasThemeSong");
+ return true;
+ }
+
+ if (query.HasThemeVideo.HasValue)
+ {
+ Logger.LogDebug("Query requires post-filtering due to HasThemeVideo");
+ return true;
+ }
+
// Filter by VideoType
if (query.VideoTypes.Length > 0)
{
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index bde10dbbf..2e7349f00 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -12,6 +12,7 @@ using System.Text.RegularExpressions;
using System.Threading;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
+using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
@@ -28,7 +29,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private const string VideotoolboxAlias = "vt";
private const string OpenclAlias = "ocl";
private const string CudaAlias = "cu";
-
+ private readonly IApplicationPaths _appPaths;
private readonly IMediaEncoder _mediaEncoder;
private readonly ISubtitleEncoder _subtitleEncoder;
@@ -51,9 +52,11 @@ namespace MediaBrowser.Controller.MediaEncoding
};
public EncodingHelper(
+ IApplicationPaths appPaths,
IMediaEncoder mediaEncoder,
ISubtitleEncoder subtitleEncoder)
{
+ _appPaths = appPaths;
_mediaEncoder = mediaEncoder;
_subtitleEncoder = subtitleEncoder;
}
@@ -81,7 +84,6 @@ namespace MediaBrowser.Controller.MediaEncoding
{ "vaapi", hwEncoder + "_vaapi" },
{ "videotoolbox", hwEncoder + "_videotoolbox" },
{ "v4l2m2m", hwEncoder + "_v4l2m2m" },
- { "omx", hwEncoder + "_omx" },
};
if (!string.IsNullOrEmpty(hwType)
@@ -578,13 +580,13 @@ namespace MediaBrowser.Controller.MediaEncoding
options);
}
- private string GetVaapiDeviceArgs(string renderNodePath, string kernelDriver, string driver, string alias)
+ private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string alias)
{
alias ??= VaapiAlias;
renderNodePath = renderNodePath ?? "/dev/dri/renderD128";
- var options = string.IsNullOrEmpty(kernelDriver) || string.IsNullOrEmpty(driver)
+ var options = string.IsNullOrEmpty(driver)
? renderNodePath
- : ",kernel_driver=" + kernelDriver + ",driver=" + driver;
+ : ",driver=" + driver + (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver);
return string.Format(
CultureInfo.InvariantCulture,
@@ -599,7 +601,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (OperatingSystem.IsLinux())
{
// derive qsv from vaapi device
- return GetVaapiDeviceArgs(null, "i915", "iHD", VaapiAlias) + arg + "@" + VaapiAlias;
+ return GetVaapiDeviceArgs(null, "iHD", "i915", VaapiAlias) + arg + "@" + VaapiAlias;
}
if (OperatingSystem.IsWindows())
@@ -688,7 +690,19 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Empty;
}
- args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, VaapiAlias));
+ if (_mediaEncoder.IsVaapiDeviceInteliHD)
+ {
+ args.Append(GetVaapiDeviceArgs(null, "iHD", null, VaapiAlias));
+ }
+ else if (_mediaEncoder.IsVaapiDeviceInteli965)
+ {
+ args.Append(GetVaapiDeviceArgs(null, "i965", null, VaapiAlias));
+ }
+ else
+ {
+ args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, VaapiAlias));
+ }
+
var filterDevArgs = GetFilterHwDeviceArgs(VaapiAlias);
if (isHwTonemapAvailable && IsOpenclFullSupported())
@@ -768,10 +782,6 @@ namespace MediaBrowser.Controller.MediaEncoding
args.Append(GetCudaDeviceArgs(0, CudaAlias))
.Append(GetFilterHwDeviceArgs(CudaAlias));
-
- // workaround for "No decoder surfaces left" error,
- // but will increase vram usage. https://trac.ffmpeg.org/ticket/7562
- args.Append(" -extra_hw_frames 3");
}
else if (string.Equals(optHwaccelType, "amf", StringComparison.OrdinalIgnoreCase))
{
@@ -832,8 +842,9 @@ namespace MediaBrowser.Controller.MediaEncoding
/// </summary>
/// <param name="state">Encoding state.</param>
/// <param name="options">Encoding options.</param>
+ /// <param name="segmentContainer">Segment Container.</param>
/// <returns>Input arguments.</returns>
- public string GetInputArgument(EncodingJobInfo state, EncodingOptions options)
+ public string GetInputArgument(EncodingJobInfo state, EncodingOptions options, string segmentContainer)
{
var arg = new StringBuilder();
var inputVidHwaccelArgs = GetInputVideoHwaccelArgs(state, options);
@@ -870,7 +881,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Also seek the external subtitles stream.
- var seekSubParam = GetFastSeekCommandLineParameter(state, options);
+ var seekSubParam = GetFastSeekCommandLineParameter(state, options, segmentContainer);
if (!string.IsNullOrEmpty(seekSubParam))
{
arg.Append(' ').Append(seekSubParam);
@@ -887,7 +898,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (state.AudioStream != null && state.AudioStream.IsExternal)
{
// Also seek the external audio stream.
- var seekAudioParam = GetFastSeekCommandLineParameter(state, options);
+ var seekAudioParam = GetFastSeekCommandLineParameter(state, options, segmentContainer);
if (!string.IsNullOrEmpty(seekAudioParam))
{
arg.Append(' ').Append(seekAudioParam);
@@ -1080,6 +1091,12 @@ namespace MediaBrowser.Controller.MediaEncoding
var alphaParam = enableAlpha ? ":alpha=1" : string.Empty;
var sub2videoParam = enableSub2video ? ":sub2video=1" : string.Empty;
+ var fontPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id);
+ var fontParam = string.Format(
+ CultureInfo.InvariantCulture,
+ ":fontsdir='{0}'",
+ _mediaEncoder.EscapeSubtitleFilterPath(fontPath));
+
// TODO
// var fallbackFontPath = Path.Combine(_appPaths.ProgramDataPath, "fonts", "DroidSansFallback.ttf");
// string fallbackFontParam = string.Empty;
@@ -1120,11 +1137,12 @@ namespace MediaBrowser.Controller.MediaEncoding
// TODO: Perhaps also use original_size=1920x800 ??
return string.Format(
CultureInfo.InvariantCulture,
- "subtitles=f='{0}'{1}{2}{3}{4}",
+ "subtitles=f='{0}'{1}{2}{3}{4}{5}",
_mediaEncoder.EscapeSubtitleFilterPath(subtitlePath),
charsetParam,
alphaParam,
sub2videoParam,
+ fontParam,
// fallbackFontParam,
setPtsParam);
}
@@ -1133,11 +1151,12 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format(
CultureInfo.InvariantCulture,
- "subtitles='{0}:si={1}{2}{3}'{4}",
+ "subtitles=f='{0}':si={1}{2}{3}{4}{5}",
_mediaEncoder.EscapeSubtitleFilterPath(mediaPath),
state.InternalSubtitleStreamOffset.ToString(CultureInfo.InvariantCulture),
alphaParam,
sub2videoParam,
+ fontParam,
// fallbackFontParam,
setPtsParam);
}
@@ -1281,11 +1300,6 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -low_power 1";
}
- if (string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
- {
- param += " -pix_fmt nv21";
- }
-
var isVc1 = string.Equals(state.VideoStream?.Codec, "vc1", StringComparison.OrdinalIgnoreCase);
var isLibX265 = string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase);
@@ -1343,29 +1357,37 @@ namespace MediaBrowser.Controller.MediaEncoding
switch (encodingOptions.EncoderPreset)
{
case "veryslow":
-
- param += " -preset slow"; // lossless is only supported on maxwell and newer(2014+)
+ param += " -preset p7";
break;
case "slow":
+ param += " -preset p6";
+ break;
+
case "slower":
- param += " -preset slow";
+ param += " -preset p5";
break;
case "medium":
- param += " -preset medium";
+ param += " -preset p4";
break;
case "fast":
+ param += " -preset p3";
+ break;
+
case "faster":
+ param += " -preset p2";
+ break;
+
case "veryfast":
case "superfast":
case "ultrafast":
- param += " -preset fast";
+ param += " -preset p1";
break;
default:
- param += " -preset default";
+ param += " -preset p4";
break;
}
}
@@ -1571,10 +1593,8 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(profile))
{
- if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
+ if (!string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
{
- // not supported by h264_omx
param += " -profile:v:0 " + profile;
}
}
@@ -1613,8 +1633,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// NVENC cannot adjust the given level, just throw an error.
// level option may cause corrupted frames on AMD VAAPI.
}
- else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
- || !string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
+ else if (!string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
{
param += " -level " + level;
}
@@ -2149,9 +2168,10 @@ namespace MediaBrowser.Controller.MediaEncoding
/// </summary>
/// <param name="state">The state.</param>
/// <param name="options">The options.</param>
+ /// <param name="segmentContainer">Segment Container.</param>
/// <returns>System.String.</returns>
/// <value>The fast seek command line parameter.</value>
- public string GetFastSeekCommandLineParameter(EncodingJobInfo state, EncodingOptions options)
+ public string GetFastSeekCommandLineParameter(EncodingJobInfo state, EncodingOptions options, string segmentContainer)
{
var time = state.BaseRequest.StartTimeTicks ?? 0;
var seekParam = string.Empty;
@@ -2163,9 +2183,13 @@ namespace MediaBrowser.Controller.MediaEncoding
if (state.IsVideoRequest)
{
var outputVideoCodec = GetVideoEncoder(state, options);
+ var segmentFormat = GetSegmentFileExtension(segmentContainer).TrimStart('.');
// Important: If this is ever re-enabled, make sure not to use it with wtv because it breaks seeking
+ // Disable -noaccurate_seek on mpegts container due to the timestamps issue on some clients,
+ // but it's still required for fMP4 container otherwise the audio can't be synced to the video.
if (!string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase)
&& state.TranscodingType != TranscodingJobType.Progressive
&& !state.EnableBreakOnNonKeyFrames(outputVideoCodec)
&& (state.BaseRequest.StartTimeTicks ?? 0) > 0)
@@ -2695,6 +2719,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty;
var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
+ var isV4l2Encoder = vidEncoder.Contains("h264_v4l2m2m", StringComparison.OrdinalIgnoreCase);
var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
@@ -2723,6 +2748,10 @@ namespace MediaBrowser.Controller.MediaEncoding
{
outFormat = "nv12";
}
+ else if (isV4l2Encoder)
+ {
+ outFormat = "yuv420p";
+ }
// sw scale
mainFilters.Add(swScaleFilter);
@@ -2773,16 +2802,15 @@ namespace MediaBrowser.Controller.MediaEncoding
var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
var isSwEncoder = !vidEncoder.Contains("nvenc", StringComparison.OrdinalIgnoreCase);
- // legacy cuvid(resize/deint/sw) pipeline(copy-back)
+ // legacy cuvid pipeline(copy-back)
if ((isSwDecoder && isSwEncoder)
|| !IsCudaFullSupported()
- || !options.EnableEnhancedNvdecDecoder
|| !_mediaEncoder.SupportsFilter("alphasrc"))
{
return GetSwVidFilterChain(state, options, vidEncoder);
}
- // prefered nvdec + cuda filters + nvenc pipeline
+ // prefered nvdec/cuvid + cuda filters + nvenc pipeline
return GetNvidiaVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
}
@@ -2800,11 +2828,11 @@ namespace MediaBrowser.Controller.MediaEncoding
var reqMaxH = state.BaseRequest.MaxHeight;
var threeDFormat = state.MediaSource.Video3DFormat;
- var isNvdecDecoder = vidDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase);
+ var isNvDecoder = vidDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase);
var isNvencEncoder = vidEncoder.Contains("nvenc", StringComparison.OrdinalIgnoreCase);
var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
var isSwEncoder = !isNvencEncoder;
- var isCuInCuOut = isNvdecDecoder && isNvencEncoder;
+ var isCuInCuOut = isNvDecoder && isNvencEncoder;
var doubleRateDeint = options.DeinterlaceDoubleRate && (state.VideoStream?.AverageFrameRate ?? 60) <= 30;
var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
@@ -2847,7 +2875,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
- if (isNvdecDecoder)
+ if (isNvDecoder)
{
// INPUT cuda surface(vram)
// hw deint
@@ -2872,7 +2900,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var memoryOutput = false;
var isUploadForOclTonemap = isSwDecoder && doCuTonemap;
- if ((isNvdecDecoder && isSwEncoder) || isUploadForOclTonemap)
+ if ((isNvDecoder && isSwEncoder) || isUploadForOclTonemap)
{
memoryOutput = true;
@@ -4268,11 +4296,6 @@ namespace MediaBrowser.Controller.MediaEncoding
{
return GetVideotoolboxVidDecoder(state, options, videoStream, bitDepth);
}
-
- if (string.Equals(options.HardwareAccelerationType, "omx", StringComparison.OrdinalIgnoreCase))
- {
- return GetOmxVidDecoder(state, options, videoStream, bitDepth);
- }
}
var whichCodec = videoStream.Codec;
@@ -4409,9 +4432,18 @@ namespace MediaBrowser.Controller.MediaEncoding
// Nvidia cuda
if (string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase))
{
- if (options.EnableEnhancedNvdecDecoder && isCudaSupported && isCodecAvailable)
+ if (isCudaSupported && isCodecAvailable)
{
- return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
+ if (options.EnableEnhancedNvdecDecoder)
+ {
+ // set -threads 1 to nvdec decoder explicitly since it doesn't implement threading support.
+ return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty) + " -threads 1" + (isAv1 ? " -c:v av1" : string.Empty);
+ }
+ else
+ {
+ // cuvid decoder doesn't have threading issue.
+ return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty);
+ }
}
}
@@ -4521,9 +4553,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return null;
}
- var hwSurface = IsCudaFullSupported()
- && options.EnableEnhancedNvdecDecoder
- && _mediaEncoder.SupportsFilter("alphasrc");
+ var hwSurface = IsCudaFullSupported() && _mediaEncoder.SupportsFilter("alphasrc");
var is8bitSwFormatsNvdec = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
var is8_10bitSwFormatsNvdec = is8bitSwFormatsNvdec || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
// TODO: add more 8/10/12bit and 4:4:4 formats for Nvdec after finishing the ffcheck tool
@@ -4747,43 +4777,6 @@ namespace MediaBrowser.Controller.MediaEncoding
return null;
}
- public string GetOmxVidDecoder(EncodingJobInfo state, EncodingOptions options, MediaStream videoStream, int bitDepth)
- {
- if (!OperatingSystem.IsLinux()
- || !string.Equals(options.HardwareAccelerationType, "omx", StringComparison.OrdinalIgnoreCase))
- {
- return null;
- }
-
- var is8bitSwFormatsOmx = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
-
- if (is8bitSwFormatsOmx)
- {
- if (string.Equals("avc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
- || string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
- {
- return GetHwDecoderName(options, "h264", "mmal", "h264", bitDepth);
- }
-
- if (string.Equals("mpeg2video", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
- {
- return GetHwDecoderName(options, "mpeg2", "mmal", "mpeg2video", bitDepth);
- }
-
- if (string.Equals("mpeg4", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
- {
- return GetHwDecoderName(options, "mpeg4", "mmal", "mpeg4", bitDepth);
- }
-
- if (string.Equals("vc1", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
- {
- return GetHwDecoderName(options, "vc1", "mmal", "vc1", bitDepth);
- }
- }
-
- return null;
- }
-
/// <summary>
/// Gets the number of threads.
/// </summary>
@@ -4848,7 +4841,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
- public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions)
+ public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions, string segmentContainer)
{
var inputModifier = string.Empty;
var probeSizeArgument = string.Empty;
@@ -4884,7 +4877,7 @@ namespace MediaBrowser.Controller.MediaEncoding
inputModifier = inputModifier.Trim();
- inputModifier += " " + GetFastSeekCommandLineParameter(state, encodingOptions);
+ inputModifier += " " + GetFastSeekCommandLineParameter(state, encodingOptions, segmentContainer);
inputModifier = inputModifier.Trim();
if (state.InputProtocol == MediaProtocol.Rtsp)
@@ -5191,13 +5184,13 @@ namespace MediaBrowser.Controller.MediaEncoding
var threads = GetNumberOfThreads(state, encodingOptions, videoCodec);
- var inputModifier = GetInputModifier(state, encodingOptions);
+ var inputModifier = GetInputModifier(state, encodingOptions, null);
return string.Format(
CultureInfo.InvariantCulture,
"{0} {1}{2} {3} {4} -map_metadata -1 -map_chapters -1 -threads {5} {6}{7}{8} -y \"{9}\"",
inputModifier,
- GetInputArgument(state, encodingOptions),
+ GetInputArgument(state, encodingOptions, null),
keyFrame,
GetMapArgs(state),
GetProgressiveVideoArguments(state, encodingOptions, videoCodec, defaultPreset),
@@ -5379,13 +5372,13 @@ namespace MediaBrowser.Controller.MediaEncoding
var threads = GetNumberOfThreads(state, encodingOptions, null);
- var inputModifier = GetInputModifier(state, encodingOptions);
+ var inputModifier = GetInputModifier(state, encodingOptions, null);
return string.Format(
CultureInfo.InvariantCulture,
"{0} {1}{7}{8} -threads {2}{3} {4} -id3v2_version 3 -write_id3v1 1{6} -y \"{5}\"",
inputModifier,
- GetInputArgument(state, encodingOptions),
+ GetInputArgument(state, encodingOptions, null),
threads,
" -vn",
string.Join(' ', audioTranscodeParams),
diff --git a/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs
index 4e7e26624..a2b6be1e6 100644
--- a/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs
@@ -6,6 +6,7 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.MediaEncoding
@@ -17,5 +18,10 @@ namespace MediaBrowser.Controller.MediaEncoding
string mediaSourceId,
int attachmentStreamIndex,
CancellationToken cancellationToken);
+ Task ExtractAllAttachments(
+ string inputFile,
+ MediaSourceInfo mediaSource,
+ string outputPath,
+ CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
index 6134c0cf3..c2ca23386 100644
--- a/MediaBrowser.Controller/Session/SessionInfo.cs
+++ b/MediaBrowser.Controller/Session/SessionInfo.cs
@@ -39,6 +39,8 @@ namespace MediaBrowser.Controller.Session
AdditionalUsers = Array.Empty<SessionUserInfo>();
PlayState = new PlayerStateInfo();
SessionControllers = Array.Empty<ISessionController>();
+ NowPlayingQueue = Array.Empty<QueueItem>();
+ NowPlayingQueueFullItems = Array.Empty<BaseItemDto>();
}
public PlayerStateInfo PlayState { get; set; }
@@ -219,7 +221,9 @@ namespace MediaBrowser.Controller.Session
}
}
- public QueueItem[] NowPlayingQueue { get; set; }
+ public IReadOnlyList<QueueItem> NowPlayingQueue { get; set; }
+
+ public IReadOnlyList<BaseItemDto> NowPlayingQueueFullItems { get; set; }
public bool HasCustomDeviceName { get; set; }
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index 3fd4cd731..06d20d90e 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -83,6 +83,130 @@ namespace MediaBrowser.MediaEncoding.Attachments
return (mediaAttachment, attachmentStream);
}
+ public async Task ExtractAllAttachments(
+ string inputFile,
+ MediaSourceInfo mediaSource,
+ string outputPath,
+ CancellationToken cancellationToken)
+ {
+ var semaphore = _semaphoreLocks.GetOrAdd(outputPath, key => new SemaphoreSlim(1, 1));
+
+ await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ if (!Directory.Exists(outputPath))
+ {
+ await ExtractAllAttachmentsInternal(
+ _mediaEncoder.GetInputArgument(inputFile, mediaSource),
+ outputPath,
+ cancellationToken).ConfigureAwait(false);
+ }
+ }
+ finally
+ {
+ semaphore.Release();
+ }
+ }
+
+ private async Task ExtractAllAttachmentsInternal(
+ string inputPath,
+ string outputPath,
+ CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(inputPath))
+ {
+ throw new ArgumentNullException(nameof(inputPath));
+ }
+
+ if (string.IsNullOrEmpty(outputPath))
+ {
+ throw new ArgumentNullException(nameof(outputPath));
+ }
+
+ Directory.CreateDirectory(outputPath);
+
+ var processArgs = string.Format(
+ CultureInfo.InvariantCulture,
+ "-dump_attachment:t \"\" -i {0} -t 0 -f null null",
+ inputPath);
+
+ int exitCode;
+
+ using (var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ Arguments = processArgs,
+ FileName = _mediaEncoder.EncoderPath,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ WorkingDirectory = outputPath,
+ ErrorDialog = false
+ },
+ EnableRaisingEvents = true
+ })
+ {
+ _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
+
+ process.Start();
+
+ var ranToCompletion = await ProcessExtensions.WaitForExitAsync(process, cancellationToken).ConfigureAwait(false);
+
+ if (!ranToCompletion)
+ {
+ try
+ {
+ _logger.LogWarning("Killing ffmpeg attachment extraction process");
+ process.Kill();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error killing attachment extraction process");
+ }
+ }
+
+ exitCode = ranToCompletion ? process.ExitCode : -1;
+ }
+
+ var failed = false;
+
+ if (exitCode != 0)
+ {
+ failed = true;
+
+ _logger.LogWarning("Deleting extracted attachments {Path} due to failure: {ExitCode}", outputPath, exitCode);
+ try
+ {
+ if (Directory.Exists(outputPath))
+ {
+ Directory.Delete(outputPath);
+ }
+ }
+ catch (IOException ex)
+ {
+ _logger.LogError(ex, "Error deleting extracted attachments {Path}", outputPath);
+ }
+ }
+ else if (!Directory.Exists(outputPath))
+ {
+ failed = true;
+ }
+
+ if (failed)
+ {
+ _logger.LogError("ffmpeg attachment extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath);
+
+ throw new InvalidOperationException(
+ string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputPath));
+ }
+ else
+ {
+ _logger.LogInformation("ffmpeg attachment extraction completed for {Path} to {Path}", inputPath, outputPath);
+ }
+ }
+
private async Task<Stream> GetAttachmentStream(
MediaSourceInfo mediaSource,
MediaAttachment mediaAttachment,
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index fe3069934..20d372d7a 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -44,18 +44,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
"mpeg4_cuvid",
"vp8_cuvid",
"vp9_cuvid",
- "av1_cuvid",
- "h264_mmal",
- "mpeg2_mmal",
- "mpeg4_mmal",
- "vc1_mmal",
- "h264_opencl",
- "hevc_opencl",
- "mpeg2_opencl",
- "mpeg4_opencl",
- "vp8_opencl",
- "vp9_opencl",
- "vc1_opencl"
+ "av1_cuvid"
};
private static readonly string[] _requiredEncoders = new[]
@@ -82,8 +71,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
"hevc_nvenc",
"h264_vaapi",
"hevc_vaapi",
- "h264_omx",
- "hevc_omx",
"h264_v4l2m2m",
"h264_videotoolbox",
"hevc_videotoolbox"
diff --git a/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs b/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs
index a1cef7a9f..c9e948780 100644
--- a/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs
+++ b/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs
@@ -12,7 +12,7 @@ namespace MediaBrowser.MediaEncoding.Probing
public class MediaChapter
{
[JsonPropertyName("id")]
- public int Id { get; set; }
+ public long Id { get; set; }
[JsonPropertyName("time_base")]
public string TimeBase { get; set; }
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 4c8f19604..8c0abe10d 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -613,6 +613,17 @@ namespace MediaBrowser.MediaEncoding.Probing
}
/// <summary>
+ /// Determines whether a stream code time base is double the frame rate.
+ /// </summary>
+ /// <param name="averageFrameRate">average frame rate.</param>
+ /// <param name="codecTimeBase">codec time base string.</param>
+ /// <returns>true if the codec time base is double the frame rate.</returns>
+ internal static bool IsCodecTimeBaseDoubleTheFrameRate(float? averageFrameRate, string codecTimeBase)
+ {
+ return MathF.Abs(((averageFrameRate ?? 0) * (GetFrameRate(codecTimeBase) ?? 0)) - 0.5f) <= float.Epsilon;
+ }
+
+ /// <summary>
/// Converts ffprobe stream info to our MediaStream class.
/// </summary>
/// <param name="isAudio">if set to <c>true</c> [is info].</param>
@@ -691,9 +702,9 @@ namespace MediaBrowser.MediaEncoding.Probing
if (string.IsNullOrEmpty(stream.Title))
{
- // mp4 missing track title workaround: fall back to handler_name if populated
+ // mp4 missing track title workaround: fall back to handler_name if populated and not the default "SoundHandler"
string handlerName = GetDictionaryValue(streamInfo.Tags, "handler_name");
- if (!string.IsNullOrEmpty(handlerName))
+ if (!string.IsNullOrEmpty(handlerName) && !string.Equals(handlerName, "SoundHandler", StringComparison.OrdinalIgnoreCase))
{
stream.Title = handlerName;
}
@@ -722,17 +733,16 @@ namespace MediaBrowser.MediaEncoding.Probing
stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate);
stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate);
+ bool videoInterlaced = !string.IsNullOrWhiteSpace(streamInfo.FieldOrder)
+ && !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase);
+
// Some interlaced H.264 files in mp4 containers using MBAFF coding aren't flagged as being interlaced by FFprobe,
// so for H.264 files we also calculate the frame rate from the codec time base and check if it is double the reported
- // frame rate (both rounded to the nearest integer) to determine if the file is interlaced
- int roundedTimeBaseFPS = Convert.ToInt32(1 / GetFrameRate(stream.CodecTimeBase) ?? 0);
- int roundedDoubleFrameRate = Convert.ToInt32(stream.AverageFrameRate * 2 ?? 0);
+ // frame rate to determine if the file is interlaced
- bool videoInterlaced = !string.IsNullOrWhiteSpace(streamInfo.FieldOrder)
- && !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase);
bool h264MbaffCoded = string.Equals(stream.Codec, "h264", StringComparison.OrdinalIgnoreCase)
&& string.IsNullOrWhiteSpace(streamInfo.FieldOrder)
- && roundedTimeBaseFPS == roundedDoubleFrameRate;
+ && IsCodecTimeBaseDoubleTheFrameRate(stream.AverageFrameRate, stream.CodecTimeBase);
if (videoInterlaced || h264MbaffCoded)
{
diff --git a/MediaBrowser.Model/Dlna/DlnaProfileType.cs b/MediaBrowser.Model/Dlna/DlnaProfileType.cs
index e30ed0f3c..c1a663bf1 100644
--- a/MediaBrowser.Model/Dlna/DlnaProfileType.cs
+++ b/MediaBrowser.Model/Dlna/DlnaProfileType.cs
@@ -6,6 +6,7 @@ namespace MediaBrowser.Model.Dlna
{
Audio = 0,
Video = 1,
- Photo = 2
+ Photo = 2,
+ Subtitle = 3
}
}
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
index 38ac44794..e2616d92a 100644
--- a/MediaBrowser.Model/Entities/MediaStream.cs
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
+using Jellyfin.Extensions;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.MediaInfo;
@@ -17,6 +18,18 @@ namespace MediaBrowser.Model.Entities
/// </summary>
public class MediaStream
{
+ private static readonly string[] _specialCodes =
+ {
+ // Uncoded languages.
+ "mis",
+ // Multiple languages.
+ "mul",
+ // Undetermined.
+ "und",
+ // No linguistic content; not applicable.
+ "zxx"
+ };
+
/// <summary>
/// Gets or sets the codec.
/// </summary>
@@ -137,7 +150,8 @@ namespace MediaBrowser.Model.Entities
{
var attributes = new List<string>();
- if (!string.IsNullOrEmpty(Language))
+ // Do not display the language code in display titles if unset or set to a special code. Show it in all other cases (possibly expanded).
+ if (!string.IsNullOrEmpty(Language) && !_specialCodes.Contains(Language, StringComparison.OrdinalIgnoreCase))
{
// Get full language string i.e. eng -> English. Will not work for some languages which use ISO 639-2/B instead of /T codes.
string fullLanguage = CultureInfo
@@ -147,7 +161,7 @@ namespace MediaBrowser.Model.Entities
attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
}
- if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase))
+ if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase) && !string.Equals(Codec, "dts", StringComparison.OrdinalIgnoreCase))
{
attributes.Add(AudioCodec.GetFriendlyName(Codec));
}
diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs
index 7207795b0..786b20e9e 100644
--- a/MediaBrowser.Model/IO/IFileSystem.cs
+++ b/MediaBrowser.Model/IO/IFileSystem.cs
@@ -200,5 +200,19 @@ namespace MediaBrowser.Model.IO
void SetAttributes(string path, bool isHidden, bool readOnly);
IEnumerable<FileSystemMetadata> GetDrives();
+
+ /// <summary>
+ /// Determines whether the directory exists.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>Whether the path exists.</returns>
+ bool DirectoryExists(string path);
+
+ /// <summary>
+ /// Determines whether the file exists.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>Whether the path exists.</returns>
+ bool FileExists(string path);
}
}
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index 4386f75af..dba3b557d 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -34,7 +34,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
- <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
<PackageReference Include="MimeTypes" Version="2.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
diff --git a/MediaBrowser.Model/Net/ISocketFactory.cs b/MediaBrowser.Model/Net/ISocketFactory.cs
index 1527ef595..a2835b711 100644
--- a/MediaBrowser.Model/Net/ISocketFactory.cs
+++ b/MediaBrowser.Model/Net/ISocketFactory.cs
@@ -26,6 +26,6 @@ namespace MediaBrowser.Model.Net
/// <param name="multicastTimeToLive">The multicast time to live value. Actually a maximum number of network hops for UDP packets.</param>
/// <param name="localPort">The local port to bind to.</param>
/// <returns>A <see cref="ISocket"/> implementation.</returns>
- ISocket CreateUdpMulticastSocket(string ipAddress, int multicastTimeToLive, int localPort);
+ ISocket CreateUdpMulticastSocket(IPAddress ipAddress, int multicastTimeToLive, int localPort);
}
}
diff --git a/MediaBrowser.Model/Querying/ItemSortBy.cs b/MediaBrowser.Model/Querying/ItemSortBy.cs
index 0b846bb96..0a28acf37 100644
--- a/MediaBrowser.Model/Querying/ItemSortBy.cs
+++ b/MediaBrowser.Model/Querying/ItemSortBy.cs
@@ -102,5 +102,9 @@ namespace MediaBrowser.Model.Querying
public const string DateLastContentAdded = "DateLastContentAdded";
public const string SeriesDatePlayed = "SeriesDatePlayed";
+
+ public const string ParentIndexNumber = "ParentIndexNumber";
+
+ public const string IndexNumber = "IndexNumber";
}
}
diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs
index 7c65fda1a..133d6a916 100644
--- a/MediaBrowser.Model/Querying/NextUpQuery.cs
+++ b/MediaBrowser.Model/Querying/NextUpQuery.cs
@@ -14,7 +14,7 @@ namespace MediaBrowser.Model.Querying
EnableTotalRecordCount = true;
DisableFirstEpisode = false;
NextUpDateCutoff = DateTime.MinValue;
- Rewatching = false;
+ EnableRewatching = false;
}
/// <summary>
@@ -86,6 +86,6 @@ namespace MediaBrowser.Model.Querying
/// <summary>
/// Gets or sets a value indicating whether getting rewatching next up list.
/// </summary>
- public bool Rewatching { get; set; }
+ public bool EnableRewatching { get; set; }
}
}
diff --git a/MediaBrowser.Model/Session/GeneralCommand.cs b/MediaBrowser.Model/Session/GeneralCommand.cs
index 29528c110..757b19b31 100644
--- a/MediaBrowser.Model/Session/GeneralCommand.cs
+++ b/MediaBrowser.Model/Session/GeneralCommand.cs
@@ -2,20 +2,26 @@
using System;
using System.Collections.Generic;
+using System.Text.Json.Serialization;
-namespace MediaBrowser.Model.Session
+namespace MediaBrowser.Model.Session;
+
+public class GeneralCommand
{
- public class GeneralCommand
+ public GeneralCommand()
+ : this(new Dictionary<string, string>())
+ {
+ }
+
+ [JsonConstructor]
+ public GeneralCommand(Dictionary<string, string> arguments)
{
- public GeneralCommand()
- {
- Arguments = new Dictionary<string, string>();
- }
+ Arguments = arguments;
+ }
- public GeneralCommandType Name { get; set; }
+ public GeneralCommandType Name { get; set; }
- public Guid ControllingUserId { get; set; }
+ public Guid ControllingUserId { get; set; }
- public Dictionary<string, string> Arguments { get; }
- }
+ public Dictionary<string, string> Arguments { get; }
}
diff --git a/MediaBrowser.Model/Session/HardwareEncodingType.cs b/MediaBrowser.Model/Session/HardwareEncodingType.cs
index 0db5697d3..f5753467a 100644
--- a/MediaBrowser.Model/Session/HardwareEncodingType.cs
+++ b/MediaBrowser.Model/Session/HardwareEncodingType.cs
@@ -21,28 +21,18 @@
NVENC = 2,
/// <summary>
- /// OpenMax OMX.
+ /// Video4Linux2 V4L2.
/// </summary>
- OMX = 3,
-
- /// <summary>
- /// Exynos V4L2 MFC.
- /// </summary>
- V4L2M2M = 4,
-
- /// <summary>
- /// MediaCodec Android.
- /// </summary>
- MediaCodec = 5,
+ V4L2M2M = 3,
/// <summary>
/// Video Acceleration API (VAAPI).
/// </summary>
- VAAPI = 6,
+ VAAPI = 4,
/// <summary>
/// Video ToolBox.
/// </summary>
- VideoToolBox = 7
+ VideoToolBox = 5
}
}
diff --git a/MediaBrowser.Providers/MediaInfo/AudioResolver.cs b/MediaBrowser.Providers/MediaInfo/AudioResolver.cs
index 425913501..0bdf447ba 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioResolver.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioResolver.cs
@@ -1,176 +1,36 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Runtime.CompilerServices;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Naming.Audio;
using Emby.Naming.Common;
-using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.IO;
namespace MediaBrowser.Providers.MediaInfo
{
/// <summary>
- /// Resolves external audios for videos.
+ /// Resolves external audio files for <see cref="Video"/>.
/// </summary>
- public class AudioResolver
+ public class AudioResolver : MediaInfoResolver
{
- private readonly ILocalizationManager _localizationManager;
- private readonly IMediaEncoder _mediaEncoder;
- private readonly NamingOptions _namingOptions;
-
/// <summary>
- /// Initializes a new instance of the <see cref="AudioResolver"/> class.
+ /// Initializes a new instance of the <see cref="AudioResolver"/> class for external audio file processing.
/// </summary>
/// <param name="localizationManager">The localization manager.</param>
/// <param name="mediaEncoder">The media encoder.</param>
- /// <param name="namingOptions">The naming options.</param>
+ /// <param name="fileSystem">The file system.</param>
+ /// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param>
public AudioResolver(
ILocalizationManager localizationManager,
IMediaEncoder mediaEncoder,
+ IFileSystem fileSystem,
NamingOptions namingOptions)
+ : base(
+ localizationManager,
+ mediaEncoder,
+ fileSystem,
+ namingOptions,
+ DlnaProfileType.Audio)
{
- _localizationManager = localizationManager;
- _mediaEncoder = mediaEncoder;
- _namingOptions = namingOptions;
- }
-
- /// <summary>
- /// Returns the audio streams found in the external audio files for the given video.
- /// </summary>
- /// <param name="video">The video to get the external audio streams from.</param>
- /// <param name="startIndex">The stream index to start adding audio streams at.</param>
- /// <param name="directoryService">The directory service to search for files.</param>
- /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
- /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
- /// <returns>A list of external audio streams.</returns>
- public async IAsyncEnumerable<MediaStream> GetExternalAudioStreams(
- Video video,
- int startIndex,
- IDirectoryService directoryService,
- bool clearCache,
- [EnumeratorCancellation] CancellationToken cancellationToken)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- if (!video.IsFileProtocol)
- {
- yield break;
- }
-
- IEnumerable<string> paths = GetExternalAudioFiles(video, directoryService, clearCache);
- foreach (string path in paths)
- {
- string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
- Model.MediaInfo.MediaInfo mediaInfo = await GetMediaInfo(path, cancellationToken).ConfigureAwait(false);
-
- foreach (MediaStream mediaStream in mediaInfo.MediaStreams)
- {
- mediaStream.Index = startIndex++;
- mediaStream.Type = MediaStreamType.Audio;
- mediaStream.IsExternal = true;
- mediaStream.Path = path;
- mediaStream.IsDefault = false;
- mediaStream.Title = null;
-
- if (string.IsNullOrEmpty(mediaStream.Language))
- {
- // Try to translate to three character code
- // Be flexible and check against both the full and three character versions
- var language = StringExtensions.RightPart(fileNameWithoutExtension, '.').ToString();
-
- if (language != fileNameWithoutExtension)
- {
- var culture = _localizationManager.FindLanguageInfo(language);
-
- language = culture == null ? language : culture.ThreeLetterISOLanguageName;
- mediaStream.Language = language;
- }
- }
-
- yield return mediaStream;
- }
- }
- }
-
- /// <summary>
- /// Returns the external audio file paths for the given video.
- /// </summary>
- /// <param name="video">The video to get the external audio file paths from.</param>
- /// <param name="directoryService">The directory service to search for files.</param>
- /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
- /// <returns>A list of external audio file paths.</returns>
- public IEnumerable<string> GetExternalAudioFiles(
- Video video,
- IDirectoryService directoryService,
- bool clearCache)
- {
- if (!video.IsFileProtocol)
- {
- yield break;
- }
-
- // Check if video folder exists
- string folder = video.ContainingFolderPath;
- if (!Directory.Exists(folder))
- {
- yield break;
- }
-
- string videoFileNameWithoutExtension = Path.GetFileNameWithoutExtension(video.Path);
-
- var files = directoryService.GetFilePaths(folder, clearCache, true);
- for (int i = 0; i < files.Count; i++)
- {
- string file = files[i];
- if (string.Equals(video.Path, file, StringComparison.OrdinalIgnoreCase)
- || !AudioFileParser.IsAudioFile(file, _namingOptions)
- || Path.GetExtension(file.AsSpan()).Equals(".strm", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file);
- // The audio filename must either be equal to the video filename or start with the video filename followed by a dot
- if (videoFileNameWithoutExtension.Equals(fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)
- || (fileNameWithoutExtension.Length > videoFileNameWithoutExtension.Length
- && fileNameWithoutExtension[videoFileNameWithoutExtension.Length] == '.'
- && fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)))
- {
- yield return file;
- }
- }
- }
-
- /// <summary>
- /// Returns the media info of the given audio file.
- /// </summary>
- /// <param name="path">The path to the audio file.</param>
- /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
- /// <returns>The media info for the given audio file.</returns>
- private Task<Model.MediaInfo.MediaInfo> GetMediaInfo(string path, CancellationToken cancellationToken)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- return _mediaEncoder.GetMediaInfo(
- new MediaInfoRequest
- {
- MediaType = DlnaProfileType.Audio,
- MediaSource = new MediaSourceInfo
- {
- Path = path,
- Protocol = MediaProtocol.File
- }
- },
- cancellationToken);
}
}
}
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
index 19a435196..fcd3f28d4 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
@@ -21,6 +21,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
@@ -39,11 +40,10 @@ namespace MediaBrowser.Providers.MediaInfo
IHasItemChangeMonitor
{
private readonly ILogger<FFProbeProvider> _logger;
- private readonly SubtitleResolver _subtitleResolver;
private readonly AudioResolver _audioResolver;
+ private readonly SubtitleResolver _subtitleResolver;
private readonly FFProbeVideoInfo _videoProber;
private readonly FFProbeAudioInfo _audioProber;
-
private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None);
public FFProbeProvider(
@@ -58,11 +58,12 @@ namespace MediaBrowser.Providers.MediaInfo
ISubtitleManager subtitleManager,
IChapterManager chapterManager,
ILibraryManager libraryManager,
+ IFileSystem fileSystem,
NamingOptions namingOptions)
{
_logger = logger;
- _audioResolver = new AudioResolver(localization, mediaEncoder, namingOptions);
- _subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager);
+ _audioResolver = new AudioResolver(localization, mediaEncoder, fileSystem, namingOptions);
+ _subtitleResolver = new SubtitleResolver(localization, mediaEncoder, fileSystem, namingOptions);
_videoProber = new FFProbeVideoInfo(
_logger,
mediaSourceManager,
@@ -75,7 +76,8 @@ namespace MediaBrowser.Providers.MediaInfo
subtitleManager,
chapterManager,
libraryManager,
- _audioResolver);
+ _audioResolver,
+ _subtitleResolver);
_audioProber = new FFProbeAudioInfo(mediaSourceManager, mediaEncoder, itemRepo, libraryManager);
}
@@ -104,7 +106,9 @@ namespace MediaBrowser.Providers.MediaInfo
if (item.SupportsLocalMetadata && video != null && !video.IsPlaceHolder
&& !video.SubtitleFiles.SequenceEqual(
- _subtitleResolver.GetExternalSubtitleFiles(video, directoryService, false), StringComparer.Ordinal))
+ _subtitleResolver.GetExternalFiles(video, directoryService, false)
+ .Select(info => info.Path).ToList(),
+ StringComparer.Ordinal))
{
_logger.LogDebug("Refreshing {ItemPath} due to external subtitles change.", item.Path);
return true;
@@ -112,7 +116,9 @@ namespace MediaBrowser.Providers.MediaInfo
if (item.SupportsLocalMetadata && video != null && !video.IsPlaceHolder
&& !video.AudioFiles.SequenceEqual(
- _audioResolver.GetExternalAudioFiles(video, directoryService, false), StringComparer.Ordinal))
+ _audioResolver.GetExternalFiles(video, directoryService, false)
+ .Select(info => info.Path).ToList(),
+ StringComparer.Ordinal))
{
_logger.LogDebug("Refreshing {ItemPath} due to external audio change.", item.Path);
return true;
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index 77a849d00..4a289b3ab 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -45,6 +45,7 @@ namespace MediaBrowser.Providers.MediaInfo
private readonly IChapterManager _chapterManager;
private readonly ILibraryManager _libraryManager;
private readonly AudioResolver _audioResolver;
+ private readonly SubtitleResolver _subtitleResolver;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly long _dummyChapterDuration = TimeSpan.FromMinutes(5).Ticks;
@@ -61,9 +62,11 @@ namespace MediaBrowser.Providers.MediaInfo
ISubtitleManager subtitleManager,
IChapterManager chapterManager,
ILibraryManager libraryManager,
- AudioResolver audioResolver)
+ AudioResolver audioResolver,
+ SubtitleResolver subtitleResolver)
{
_logger = logger;
+ _mediaSourceManager = mediaSourceManager;
_mediaEncoder = mediaEncoder;
_itemRepo = itemRepo;
_blurayExaminer = blurayExaminer;
@@ -74,7 +77,7 @@ namespace MediaBrowser.Providers.MediaInfo
_chapterManager = chapterManager;
_libraryManager = libraryManager;
_audioResolver = audioResolver;
- _mediaSourceManager = mediaSourceManager;
+ _subtitleResolver = subtitleResolver;
}
public async Task<ItemUpdateType> ProbeVideo<T>(
@@ -215,7 +218,7 @@ namespace MediaBrowser.Providers.MediaInfo
chapters = Array.Empty<ChapterInfo>();
}
- await AddExternalSubtitles(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
+ await AddExternalSubtitlesAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
await AddExternalAudioAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
@@ -526,16 +529,14 @@ namespace MediaBrowser.Providers.MediaInfo
/// <param name="options">The refreshOptions.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- private async Task AddExternalSubtitles(
+ private async Task AddExternalSubtitlesAsync(
Video video,
List<MediaStream> currentStreams,
MetadataRefreshOptions options,
CancellationToken cancellationToken)
{
- var subtitleResolver = new SubtitleResolver(_localization);
-
var startIndex = currentStreams.Count == 0 ? 0 : (currentStreams.Select(i => i.Index).Max() + 1);
- var externalSubtitleStreams = subtitleResolver.GetExternalSubtitleStreams(video, startIndex, options.DirectoryService, false);
+ var externalSubtitleStreams = await _subtitleResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, false, cancellationToken);
var enableSubtitleDownloading = options.MetadataRefreshMode == MetadataRefreshMode.Default ||
options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh;
@@ -589,11 +590,11 @@ namespace MediaBrowser.Providers.MediaInfo
// Rescan
if (downloadedLanguages.Count > 0)
{
- externalSubtitleStreams = subtitleResolver.GetExternalSubtitleStreams(video, startIndex, options.DirectoryService, true);
+ externalSubtitleStreams = await _subtitleResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, true, cancellationToken);
}
}
- video.SubtitleFiles = externalSubtitleStreams.Select(i => i.Path).ToArray();
+ video.SubtitleFiles = externalSubtitleStreams.Select(i => i.Path).Distinct().ToArray();
currentStreams.AddRange(externalSubtitleStreams);
}
@@ -612,15 +613,11 @@ namespace MediaBrowser.Providers.MediaInfo
CancellationToken cancellationToken)
{
var startIndex = currentStreams.Count == 0 ? 0 : currentStreams.Max(i => i.Index) + 1;
- var externalAudioStreams = _audioResolver.GetExternalAudioStreams(video, startIndex, options.DirectoryService, false, cancellationToken);
+ var externalAudioStreams = await _audioResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, false, cancellationToken).ConfigureAwait(false);
- await foreach (MediaStream externalAudioStream in externalAudioStreams)
- {
- currentStreams.Add(externalAudioStream);
- }
+ video.AudioFiles = externalAudioStreams.Select(i => i.Path).Distinct().ToArray();
- // Select all external audio file paths
- video.AudioFiles = currentStreams.Where(i => i.Type == MediaStreamType.Audio && i.IsExternal).Select(i => i.Path).Distinct().ToArray();
+ currentStreams.AddRange(externalAudioStreams);
}
/// <summary>
diff --git a/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs b/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs
new file mode 100644
index 000000000..39be405ec
--- /dev/null
+++ b/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs
@@ -0,0 +1,232 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Naming.Common;
+using Emby.Naming.ExternalFiles;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+
+namespace MediaBrowser.Providers.MediaInfo
+{
+ /// <summary>
+ /// Resolves external files for <see cref="Video"/>.
+ /// </summary>
+ public abstract class MediaInfoResolver
+ {
+ /// <summary>
+ /// The <see cref="ExternalPathParser"/> instance.
+ /// </summary>
+ private readonly ExternalPathParser _externalPathParser;
+
+ /// <summary>
+ /// The <see cref="IMediaEncoder"/> instance.
+ /// </summary>
+ private readonly IMediaEncoder _mediaEncoder;
+
+ private readonly IFileSystem _fileSystem;
+
+ /// <summary>
+ /// The <see cref="NamingOptions"/> instance.
+ /// </summary>
+ private readonly NamingOptions _namingOptions;
+
+ /// <summary>
+ /// The <see cref="DlnaProfileType"/> of the files this resolver should resolve.
+ /// </summary>
+ private readonly DlnaProfileType _type;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MediaInfoResolver"/> class.
+ /// </summary>
+ /// <param name="localizationManager">The localization manager.</param>
+ /// <param name="mediaEncoder">The media encoder.</param>
+ /// <param name="fileSystem">The file system.</param>
+ /// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param>
+ /// <param name="type">The <see cref="DlnaProfileType"/> of the parsed file.</param>
+ protected MediaInfoResolver(
+ ILocalizationManager localizationManager,
+ IMediaEncoder mediaEncoder,
+ IFileSystem fileSystem,
+ NamingOptions namingOptions,
+ DlnaProfileType type)
+ {
+ _mediaEncoder = mediaEncoder;
+ _fileSystem = fileSystem;
+ _namingOptions = namingOptions;
+ _type = type;
+ _externalPathParser = new ExternalPathParser(namingOptions, localizationManager, _type);
+ }
+
+ /// <summary>
+ /// Retrieves the external streams for the provided video.
+ /// </summary>
+ /// <param name="video">The <see cref="Video"/> object to search external streams for.</param>
+ /// <param name="startIndex">The stream index to start adding external streams at.</param>
+ /// <param name="directoryService">The directory service to search for files.</param>
+ /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The external streams located.</returns>
+ public async Task<IReadOnlyList<MediaStream>> GetExternalStreamsAsync(
+ Video video,
+ int startIndex,
+ IDirectoryService directoryService,
+ bool clearCache,
+ CancellationToken cancellationToken)
+ {
+ if (!video.IsFileProtocol)
+ {
+ return Array.Empty<MediaStream>();
+ }
+
+ var pathInfos = GetExternalFiles(video, directoryService, clearCache);
+
+ if (!pathInfos.Any())
+ {
+ return Array.Empty<MediaStream>();
+ }
+
+ var mediaStreams = new List<MediaStream>();
+
+ foreach (var pathInfo in pathInfos)
+ {
+ var mediaInfo = await GetMediaInfo(pathInfo.Path, _type, cancellationToken).ConfigureAwait(false);
+
+ if (mediaInfo.MediaStreams.Count == 1)
+ {
+ MediaStream mediaStream = mediaInfo.MediaStreams[0];
+ mediaStream.Index = startIndex++;
+ mediaStream.IsDefault = pathInfo.IsDefault || mediaStream.IsDefault;
+ mediaStream.IsForced = pathInfo.IsForced || mediaStream.IsForced;
+
+ mediaStreams.Add(MergeMetadata(mediaStream, pathInfo));
+ }
+ else
+ {
+ foreach (MediaStream mediaStream in mediaInfo.MediaStreams)
+ {
+ mediaStream.Index = startIndex++;
+
+ mediaStreams.Add(MergeMetadata(mediaStream, pathInfo));
+ }
+ }
+ }
+
+ return mediaStreams.AsReadOnly();
+ }
+
+ /// <summary>
+ /// Returns the external file infos for the given video.
+ /// </summary>
+ /// <param name="video">The <see cref="Video"/> object to search external files for.</param>
+ /// <param name="directoryService">The directory service to search for files.</param>
+ /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
+ /// <returns>The external file paths located.</returns>
+ public IReadOnlyList<ExternalPathParserResult> GetExternalFiles(
+ Video video,
+ IDirectoryService directoryService,
+ bool clearCache)
+ {
+ if (!video.IsFileProtocol)
+ {
+ return Array.Empty<ExternalPathParserResult>();
+ }
+
+ // Check if video folder exists
+ string folder = video.ContainingFolderPath;
+ if (!_fileSystem.DirectoryExists(folder))
+ {
+ return Array.Empty<ExternalPathParserResult>();
+ }
+
+ var files = directoryService.GetFilePaths(folder, clearCache).ToList();
+ var internalMetadataPath = video.GetInternalMetadataPath();
+ if (_fileSystem.DirectoryExists(internalMetadataPath))
+ {
+ files.AddRange(directoryService.GetFilePaths(internalMetadataPath, clearCache));
+ }
+
+ if (!files.Any())
+ {
+ return Array.Empty<ExternalPathParserResult>();
+ }
+
+ var externalPathInfos = new List<ExternalPathParserResult>();
+ ReadOnlySpan<char> prefix = video.FileNameWithoutExtension;
+ foreach (var file in files)
+ {
+ var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file.AsSpan());
+ if (fileNameWithoutExtension.Length >= prefix.Length
+ && prefix.Equals(fileNameWithoutExtension[..prefix.Length], StringComparison.OrdinalIgnoreCase)
+ && (fileNameWithoutExtension.Length == prefix.Length || _namingOptions.MediaFlagDelimiters.Contains(fileNameWithoutExtension[prefix.Length])))
+ {
+ var externalPathInfo = _externalPathParser.ParseFile(file, fileNameWithoutExtension[prefix.Length..].ToString());
+
+ if (externalPathInfo != null)
+ {
+ externalPathInfos.Add(externalPathInfo);
+ }
+ }
+ }
+
+ return externalPathInfos;
+ }
+
+ /// <summary>
+ /// Returns the media info of the given file.
+ /// </summary>
+ /// <param name="path">The path to the file.</param>
+ /// <param name="type">The <see cref="DlnaProfileType"/>.</param>
+ /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
+ /// <returns>The media info for the given file.</returns>
+ private Task<Model.MediaInfo.MediaInfo> GetMediaInfo(string path, DlnaProfileType type, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ return _mediaEncoder.GetMediaInfo(
+ new MediaInfoRequest
+ {
+ MediaType = type,
+ MediaSource = new MediaSourceInfo
+ {
+ Path = path,
+ Protocol = MediaProtocol.File
+ }
+ },
+ cancellationToken);
+ }
+
+ /// <summary>
+ /// Merges path metadata into stream metadata.
+ /// </summary>
+ /// <param name="mediaStream">The <see cref="MediaStream"/> object.</param>
+ /// <param name="pathInfo">The <see cref="ExternalPathParserResult"/> object.</param>
+ /// <returns>The modified mediaStream.</returns>
+ private MediaStream MergeMetadata(MediaStream mediaStream, ExternalPathParserResult pathInfo)
+ {
+ mediaStream.Path = pathInfo.Path;
+ mediaStream.IsExternal = true;
+ mediaStream.Title = string.IsNullOrEmpty(mediaStream.Title) ? (string.IsNullOrEmpty(pathInfo.Title) ? null : pathInfo.Title) : mediaStream.Title;
+ mediaStream.Language = string.IsNullOrEmpty(mediaStream.Language) ? (string.IsNullOrEmpty(pathInfo.Language) ? null : pathInfo.Language) : mediaStream.Language;
+
+ mediaStream.Type = _type switch
+ {
+ DlnaProfileType.Audio => MediaStreamType.Audio,
+ DlnaProfileType.Subtitle => MediaStreamType.Subtitle,
+ _ => mediaStream.Type
+ };
+
+ return mediaStream;
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
index ba284187e..4b9ba944a 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
@@ -1,235 +1,36 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
+using Emby.Naming.Common;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.IO;
namespace MediaBrowser.Providers.MediaInfo
{
/// <summary>
- /// Resolves external subtitles for videos.
+ /// Resolves external subtitle files for <see cref="Video"/>.
/// </summary>
- public class SubtitleResolver
+ public class SubtitleResolver : MediaInfoResolver
{
- private readonly ILocalizationManager _localization;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SubtitleResolver"/> class.
- /// </summary>
- /// <param name="localization">The localization manager.</param>
- public SubtitleResolver(ILocalizationManager localization)
- {
- _localization = localization;
- }
-
- /// <summary>
- /// Retrieves the external subtitle streams for the provided video.
- /// </summary>
- /// <param name="video">The video to search from.</param>
- /// <param name="startIndex">The stream index to start adding subtitle streams at.</param>
- /// <param name="directoryService">The directory service to search for files.</param>
- /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
- /// <returns>The external subtitle streams located.</returns>
- public List<MediaStream> GetExternalSubtitleStreams(
- Video video,
- int startIndex,
- IDirectoryService directoryService,
- bool clearCache)
- {
- var streams = new List<MediaStream>();
-
- if (!video.IsFileProtocol)
- {
- return streams;
- }
-
- AddExternalSubtitleStreams(streams, video.ContainingFolderPath, video.Path, startIndex, directoryService, clearCache);
-
- startIndex += streams.Count;
-
- string folder = video.GetInternalMetadataPath();
-
- if (!Directory.Exists(folder))
- {
- return streams;
- }
-
- try
- {
- AddExternalSubtitleStreams(streams, folder, video.Path, startIndex, directoryService, clearCache);
- }
- catch (IOException)
- {
- }
-
- return streams;
- }
-
- /// <summary>
- /// Locates the external subtitle files for the provided video.
- /// </summary>
- /// <param name="video">The video to search from.</param>
- /// <param name="directoryService">The directory service to search for files.</param>
- /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
- /// <returns>The external subtitle file paths located.</returns>
- public IEnumerable<string> GetExternalSubtitleFiles(
- Video video,
- IDirectoryService directoryService,
- bool clearCache)
- {
- if (!video.IsFileProtocol)
- {
- yield break;
- }
-
- var streams = GetExternalSubtitleStreams(video, 0, directoryService, clearCache);
-
- foreach (var stream in streams)
- {
- yield return stream.Path;
- }
- }
-
/// <summary>
- /// Extracts the subtitle files from the provided list and adds them to the list of streams.
+ /// Initializes a new instance of the <see cref="SubtitleResolver"/> class for external subtitle file processing.
/// </summary>
- /// <param name="streams">The list of streams to add external subtitles to.</param>
- /// <param name="videoPath">The path to the video file.</param>
- /// <param name="startIndex">The stream index to start adding subtitle streams at.</param>
- /// <param name="files">The files to add if they are subtitles.</param>
- public void AddExternalSubtitleStreams(
- List<MediaStream> streams,
- string videoPath,
- int startIndex,
- IReadOnlyList<string> files)
- {
- var videoFileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(videoPath);
-
- for (var i = 0; i < files.Count; i++)
- {
- var fullName = files[i];
- var extension = Path.GetExtension(fullName.AsSpan());
- if (!IsSubtitleExtension(extension))
- {
- continue;
- }
-
- var fileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(fullName);
-
- MediaStream mediaStream;
-
- // The subtitle filename must either be equal to the video filename or start with the video filename followed by a dot
- if (videoFileNameWithoutExtension.Equals(fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
- {
- mediaStream = new MediaStream
- {
- Index = startIndex++,
- Type = MediaStreamType.Subtitle,
- IsExternal = true,
- Path = fullName
- };
- }
- else if (fileNameWithoutExtension.Length > videoFileNameWithoutExtension.Length
- && fileNameWithoutExtension[videoFileNameWithoutExtension.Length] == '.'
- && fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
- {
- var isForced = fullName.Contains(".forced.", StringComparison.OrdinalIgnoreCase)
- || fullName.Contains(".foreign.", StringComparison.OrdinalIgnoreCase);
-
- var isDefault = fullName.Contains(".default.", StringComparison.OrdinalIgnoreCase);
-
- // Support xbmc naming conventions - 300.spanish.srt
- var languageSpan = fileNameWithoutExtension;
- while (languageSpan.Length > 0)
- {
- var lastDot = languageSpan.LastIndexOf('.');
- if (lastDot < videoFileNameWithoutExtension.Length)
- {
- languageSpan = ReadOnlySpan<char>.Empty;
- break;
- }
-
- var currentSlice = languageSpan[lastDot..];
- if (currentSlice.Equals(".default", StringComparison.OrdinalIgnoreCase)
- || currentSlice.Equals(".forced", StringComparison.OrdinalIgnoreCase)
- || currentSlice.Equals(".foreign", StringComparison.OrdinalIgnoreCase))
- {
- languageSpan = languageSpan[..lastDot];
- continue;
- }
-
- languageSpan = languageSpan[(lastDot + 1)..];
- break;
- }
-
- var language = languageSpan.ToString();
- if (string.IsNullOrWhiteSpace(language))
- {
- language = null;
- }
- else
- {
- // Try to translate to three character code
- // Be flexible and check against both the full and three character versions
- var culture = _localization.FindLanguageInfo(language);
-
- language = culture == null ? language : culture.ThreeLetterISOLanguageName;
- }
-
- mediaStream = new MediaStream
- {
- Index = startIndex++,
- Type = MediaStreamType.Subtitle,
- IsExternal = true,
- Path = fullName,
- Language = language,
- IsForced = isForced,
- IsDefault = isDefault
- };
- }
- else
- {
- continue;
- }
-
- mediaStream.Codec = extension.TrimStart('.').ToString().ToLowerInvariant();
-
- streams.Add(mediaStream);
- }
- }
-
- private static bool IsSubtitleExtension(ReadOnlySpan<char> extension)
- {
- return extension.Equals(".srt", StringComparison.OrdinalIgnoreCase)
- || extension.Equals(".ssa", StringComparison.OrdinalIgnoreCase)
- || extension.Equals(".ass", StringComparison.OrdinalIgnoreCase)
- || extension.Equals(".sub", StringComparison.OrdinalIgnoreCase)
- || extension.Equals(".vtt", StringComparison.OrdinalIgnoreCase)
- || extension.Equals(".smi", StringComparison.OrdinalIgnoreCase)
- || extension.Equals(".sami", StringComparison.OrdinalIgnoreCase);
- }
-
- private static ReadOnlySpan<char> NormalizeFilenameForSubtitleComparison(string filename)
- {
- // Try to account for sloppy file naming
- filename = filename.Replace("_", string.Empty, StringComparison.Ordinal);
- filename = filename.Replace(" ", string.Empty, StringComparison.Ordinal);
- return Path.GetFileNameWithoutExtension(filename.AsSpan());
- }
-
- private void AddExternalSubtitleStreams(
- List<MediaStream> streams,
- string folder,
- string videoPath,
- int startIndex,
- IDirectoryService directoryService,
- bool clearCache)
+ /// <param name="localizationManager">The localization manager.</param>
+ /// <param name="mediaEncoder">The media encoder.</param>
+ /// <param name="fileSystem">The file system.</param>
+ /// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param>
+ public SubtitleResolver(
+ ILocalizationManager localizationManager,
+ IMediaEncoder mediaEncoder,
+ IFileSystem fileSystem,
+ NamingOptions namingOptions)
+ : base(
+ localizationManager,
+ mediaEncoder,
+ fileSystem,
+ namingOptions,
+ DlnaProfileType.Subtitle)
{
- var files = directoryService.GetFilePaths(folder, clearCache, true);
-
- AddExternalSubtitleStreams(streams, videoPath, startIndex, files);
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs b/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs
index e0ab31b56..f4941565f 100644
--- a/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs
@@ -12,8 +12,7 @@ namespace MediaBrowser.Providers.Plugins.StudioImages
{
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
{
- // TODO change this for a Jellyfin-hosted repository.
- public const string DefaultServer = "https://raw.github.com/MediaBrowser/MediaBrowser.Resources/master/images/imagesbyname";
+ public const string DefaultServer = "https://raw.github.com/jellyfin/emby-artwork/master/studios";
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
diff --git a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
index 3a3048cec..ce267ac84 100644
--- a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
@@ -50,41 +50,29 @@ namespace MediaBrowser.Providers.Studios
{
return new List<ImageType>
{
- ImageType.Primary,
ImageType.Thumb
};
}
- public Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
+ public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
- return GetImages(item, true, true, cancellationToken);
- }
-
- private async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, bool posters, bool thumbs, CancellationToken cancellationToken)
- {
- var list = new List<RemoteImageInfo>();
-
- if (posters)
- {
- var posterPath = Path.Combine(_config.ApplicationPaths.CachePath, "imagesbyname", "remotestudioposters.txt");
+ var thumbsPath = Path.Combine(_config.ApplicationPaths.CachePath, "imagesbyname", "remotestudiothumbs.txt");
- posterPath = await EnsurePosterList(posterPath, cancellationToken).ConfigureAwait(false);
-
- list.Add(GetImage(item, posterPath, ImageType.Primary, "folder"));
- }
+ thumbsPath = await EnsureThumbsList(thumbsPath, cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
- if (thumbs)
- {
- var thumbsPath = Path.Combine(_config.ApplicationPaths.CachePath, "imagesbyname", "remotestudiothumbs.txt");
-
- thumbsPath = await EnsureThumbsList(thumbsPath, cancellationToken).ConfigureAwait(false);
+ var imageInfo = GetImage(item, thumbsPath, ImageType.Thumb, "thumb");
- list.Add(GetImage(item, thumbsPath, ImageType.Thumb, "thumb"));
+ if (imageInfo == null)
+ {
+ return Enumerable.Empty<RemoteImageInfo>();
}
- return list.Where(i => i != null);
+ return new RemoteImageInfo[]
+ {
+ imageInfo
+ };
}
private RemoteImageInfo GetImage(BaseItem item, string filename, ImageType type, string remoteFilename)
@@ -110,19 +98,12 @@ namespace MediaBrowser.Providers.Studios
private string GetUrl(string image, string filename)
{
- return string.Format(CultureInfo.InvariantCulture, "{0}/{1}/{2}.jpg", repositoryUrl, image, filename);
+ return string.Format(CultureInfo.InvariantCulture, "{0}/images/{1}/{2}.jpg", repositoryUrl, image, filename);
}
private Task<string> EnsureThumbsList(string file, CancellationToken cancellationToken)
{
- string url = string.Format(CultureInfo.InvariantCulture, "{0}/studiothumbs.txt", repositoryUrl);
-
- return EnsureList(url, file, _fileSystem, cancellationToken);
- }
-
- private Task<string> EnsurePosterList(string file, CancellationToken cancellationToken)
- {
- string url = string.Format(CultureInfo.InvariantCulture, "{0}/studioposters.txt", repositoryUrl);
+ string url = string.Format(CultureInfo.InvariantCulture, "{0}/thumbs.txt", repositoryUrl);
return EnsureList(url, file, _fileSystem, cancellationToken);
}
diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs
index a66b70ac1..6e4f5634d 100644
--- a/RSSDP/SsdpCommunicationsServer.cs
+++ b/RSSDP/SsdpCommunicationsServer.cs
@@ -338,7 +338,7 @@ namespace Rssdp.Infrastructure
private ISocket ListenForBroadcastsAsync()
{
- var socket = _SocketFactory.CreateUdpMulticastSocket(SsdpConstants.MulticastLocalAdminAddress, _MulticastTtl, SsdpConstants.MulticastPort);
+ var socket = _SocketFactory.CreateUdpMulticastSocket(IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress), _MulticastTtl, SsdpConstants.MulticastPort);
_ = ListenToSocketInternal(socket);
return socket;
diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64
index 350f0076a..a4638c03b 100644
--- a/deployment/Dockerfile.centos.amd64
+++ b/deployment/Dockerfile.centos.amd64
@@ -13,7 +13,7 @@ RUN yum update -yq \
&& yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/e7acb87d-ab08-4620-9050-b3e80f688d36/e93bbadc19b12f81e3a6761719f28b47/dotnet-sdk-6.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c505a449-9ecf-4352-8629-56216f521616/bd6807340faae05b61de340c8bf161e8/dotnet-sdk-6.0.201-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64
index eeff9a96f..8f564bb12 100644
--- a/deployment/Dockerfile.fedora.amd64
+++ b/deployment/Dockerfile.fedora.amd64
@@ -12,7 +12,7 @@ RUN dnf update -yq \
&& dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/e7acb87d-ab08-4620-9050-b3e80f688d36/e93bbadc19b12f81e3a6761719f28b47/dotnet-sdk-6.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c505a449-9ecf-4352-8629-56216f521616/bd6807340faae05b61de340c8bf161e8/dotnet-sdk-6.0.201-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
index 9d2deb1c6..f5a5b54fc 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -17,7 +17,7 @@ RUN apt-get update -yqq \
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/e7acb87d-ab08-4620-9050-b3e80f688d36/e93bbadc19b12f81e3a6761719f28b47/dotnet-sdk-6.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c505a449-9ecf-4352-8629-56216f521616/bd6807340faae05b61de340c8bf161e8/dotnet-sdk-6.0.201-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64
index ec90dba83..7ca0f6470 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/e7acb87d-ab08-4620-9050-b3e80f688d36/e93bbadc19b12f81e3a6761719f28b47/dotnet-sdk-6.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c505a449-9ecf-4352-8629-56216f521616/bd6807340faae05b61de340c8bf161e8/dotnet-sdk-6.0.201-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf
index 3685e16c4..384e49bf0 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/e7acb87d-ab08-4620-9050-b3e80f688d36/e93bbadc19b12f81e3a6761719f28b47/dotnet-sdk-6.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c505a449-9ecf-4352-8629-56216f521616/bd6807340faae05b61de340c8bf161e8/dotnet-sdk-6.0.201-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs b/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs
index d80925fc9..a7d52184b 100644
--- a/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs
+++ b/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs
@@ -39,13 +39,13 @@ public class KeyframeExtractionScheduledTask : IScheduledTask
}
/// <inheritdoc />
- public string Name => "Keyframe Extractor";
+ public string Name => _localizationManager.GetLocalizedString("TaskKeyframeExtractor");
/// <inheritdoc />
public string Key => "KeyframeExtraction";
/// <inheritdoc />
- public string Description => "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.";
+ public string Description => _localizationManager.GetLocalizedString("TaskKeyframeExtractorDescription");
/// <inheritdoc />
public string Category => _localizationManager.GetLocalizedString("TasksLibraryCategory");
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
index 31da11e24..d5e36cb23 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
+++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
@@ -21,7 +21,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
</ItemGroup>
<ItemGroup>
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index 38fb37f17..6f3b83455 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -15,13 +15,13 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.2" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.3" />
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
- <PackageReference Include="Moq" Version="4.16.1" />
+ <PackageReference Include="Moq" Version="4.17.2" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
index 7b20823ba..1ad0f4e00 100644
--- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -12,7 +12,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
index 374a17e2f..9e6f7c0c1 100644
--- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
+++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
@@ -12,8 +12,8 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
- <PackageReference Include="Moq" Version="4.16.1" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
+ <PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
index d178837b2..4918e2e82 100644
--- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
+++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
@@ -7,8 +7,8 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
- <PackageReference Include="Moq" Version="4.16.1" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
+ <PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
diff --git a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
index 725947577..55125eb11 100644
--- a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
+++ b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
@@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
diff --git a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj
index 7ab34775a..c53dab6d8 100644
--- a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj
@@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
diff --git a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj
index 639c84240..268631e58 100644
--- a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj
@@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
index e7534e308..1a7c21084 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
@@ -22,8 +22,8 @@
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
- <PackageReference Include="Moq" Version="4.16.1" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
+ <PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
</ItemGroup>
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
index 0fc8724b6..53e1550ed 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
@@ -31,6 +31,16 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
public void GetFrameRate_Success(string value, float? expected)
=> Assert.Equal(expected, ProbeResultNormalizer.GetFrameRate(value));
+ [Theory]
+ [InlineData(0.5f, "0/1", false)]
+ [InlineData(24.5f, "8/196", false)]
+ [InlineData(63.5f, "1/127", true)]
+ [InlineData(null, "1/60", false)]
+ [InlineData(30f, "2/120", true)]
+ [InlineData(59.999996f, "1563/187560", true)]
+ public void IsCodecTimeBaseDoubleTheFrameRate_Success(float? frameRate, string codecTimeBase, bool expected)
+ => Assert.Equal(expected, ProbeResultNormalizer.IsCodecTimeBaseDoubleTheFrameRate(frameRate, codecTimeBase));
+
[Fact]
public void GetMediaInfo_MetaData_Success()
{
diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
index 22db5bea2..9da80c312 100644
--- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
+++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
@@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
diff --git a/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs b/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs
new file mode 100644
index 000000000..b396b5440
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs
@@ -0,0 +1,111 @@
+using System.Text.RegularExpressions;
+using Emby.Naming.Common;
+using Emby.Naming.ExternalFiles;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Globalization;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.ExternalFiles;
+
+public class ExternalPathParserTests
+{
+ private readonly ExternalPathParser _audioPathParser;
+ private readonly ExternalPathParser _subtitlePathParser;
+
+ public ExternalPathParserTests()
+ {
+ var englishCultureDto = new CultureDto("English", "English", "en", new[] { "eng" });
+ var frenchCultureDto = new CultureDto("French", "French", "fr", new[] { "fre", "fra" });
+
+ var localizationManager = new Mock<ILocalizationManager>(MockBehavior.Loose);
+ localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"en.*", RegexOptions.IgnoreCase)))
+ .Returns(englishCultureDto);
+ localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"fr.*", RegexOptions.IgnoreCase)))
+ .Returns(frenchCultureDto);
+
+ _audioPathParser = new ExternalPathParser(new NamingOptions(), localizationManager.Object, DlnaProfileType.Audio);
+ _subtitlePathParser = new ExternalPathParser(new NamingOptions(), localizationManager.Object, DlnaProfileType.Subtitle);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData("MyVideo.ass")]
+ [InlineData("MyVideo.mks")]
+ [InlineData("MyVideo.sami")]
+ [InlineData("MyVideo.srt")]
+ [InlineData("MyVideo.m4v")]
+ public void ParseFile_AudioExtensionsNotMatched_ReturnsNull(string path)
+ {
+ Assert.Null(_audioPathParser.ParseFile(path, string.Empty));
+ }
+
+ [Theory]
+ [InlineData("MyVideo.aa")]
+ [InlineData("MyVideo.aac")]
+ [InlineData("MyVideo.flac")]
+ [InlineData("MyVideo.m4a")]
+ [InlineData("MyVideo.mka")]
+ [InlineData("MyVideo.mp3")]
+ public void ParseFile_AudioExtensionsMatched_ReturnsPath(string path)
+ {
+ var actual = _audioPathParser.ParseFile(path, string.Empty);
+ Assert.NotNull(actual);
+ Assert.Equal(path, actual!.Path);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData("MyVideo.aa")]
+ [InlineData("MyVideo.aac")]
+ [InlineData("MyVideo.flac")]
+ [InlineData("MyVideo.mka")]
+ [InlineData("MyVideo.m4v")]
+ public void ParseFile_SubtitleExtensionsNotMatched_ReturnsNull(string path)
+ {
+ Assert.Null(_subtitlePathParser.ParseFile(path, string.Empty));
+ }
+
+ [Theory]
+ [InlineData("MyVideo.ass")]
+ [InlineData("MyVideo.mks")]
+ [InlineData("MyVideo.sami")]
+ [InlineData("MyVideo.srt")]
+ [InlineData("MyVideo.vtt")]
+ public void ParseFile_SubtitleExtensionsMatched_ReturnsPath(string path)
+ {
+ var actual = _subtitlePathParser.ParseFile(path, string.Empty);
+ Assert.NotNull(actual);
+ Assert.Equal(path, actual!.Path);
+ }
+
+ [Theory]
+ [InlineData("", null, null)]
+ [InlineData(".default", null, null, true, false)]
+ [InlineData(".forced", null, null, false, true)]
+ [InlineData(".foreign", null, null, false, true)]
+ [InlineData(".default.forced", null, null, true, true)]
+ [InlineData(".forced.default", null, null, true, true)]
+ [InlineData(".DEFAULT.FORCED", null, null, true, true)]
+ [InlineData(".en", null, "eng")]
+ [InlineData(".EN", null, "eng")]
+ [InlineData(".fr.en", "fr", "eng")]
+ [InlineData(".en.fr", "en", "fre")]
+ [InlineData(".title.en.fr", "title.en", "fre")]
+ [InlineData(".Title Goes Here", "Title Goes Here", null)]
+ [InlineData(".Title.with.Separator", "Title.with.Separator", null)]
+ [InlineData(".title.en.default.forced", "title", "eng", true, true)]
+ [InlineData(".forced.default.en.title", "title", "eng", true, true)]
+ public void ParseFile_ExtraTokens_ParseToValues(string tokens, string? title, string? language, bool isDefault = false, bool isForced = false)
+ {
+ var path = "My.Video" + tokens + ".srt";
+
+ var actual = _subtitlePathParser.ParseFile(path, tokens);
+
+ Assert.NotNull(actual);
+ Assert.Equal(title, actual!.Title);
+ Assert.Equal(language, actual.Language);
+ Assert.Equal(isDefault, actual.IsDefault);
+ Assert.Equal(isForced, actual.IsForced);
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
index 59b7e1cbe..ea86906e7 100644
--- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
+++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
@@ -12,7 +12,8 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
+ <PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
diff --git a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs
deleted file mode 100644
index 2446660f3..000000000
--- a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using Emby.Naming.Common;
-using Emby.Naming.Subtitles;
-using Xunit;
-
-namespace Jellyfin.Naming.Tests.Subtitles
-{
- public class SubtitleParserTests
- {
- private readonly NamingOptions _namingOptions = new NamingOptions();
-
- [Theory]
- [InlineData("The Skin I Live In (2011).srt", null, false, false)]
- [InlineData("The Skin I Live In (2011).eng.srt", "eng", false, false)]
- [InlineData("The Skin I Live In (2011).eng.default.srt", "eng", true, false)]
- [InlineData("The Skin I Live In (2011).eng.forced.srt", "eng", false, true)]
- [InlineData("The Skin I Live In (2011).eng.foreign.srt", "eng", false, true)]
- [InlineData("The Skin I Live In (2011).eng.default.foreign.srt", "eng", true, true)]
- [InlineData("The Skin I Live In (2011).default.foreign.eng.srt", "eng", true, true)]
- public void SubtitleParser_ValidFileName_Parses(string input, string language, bool isDefault, bool isForced)
- {
- var parser = new SubtitleParser(_namingOptions);
-
- var result = parser.ParseFile(input);
-
- Assert.Equal(language, result?.Language, true);
- Assert.Equal(isDefault, result?.IsDefault);
- Assert.Equal(isForced, result?.IsForced);
- Assert.Equal(input, result?.Path);
- }
-
- [Theory]
- [InlineData("The Skin I Live In (2011).mp4")]
- [InlineData("")]
- public void SubtitleParser_InvalidFileName_ReturnsNull(string input)
- {
- var parser = new SubtitleParser(_namingOptions);
-
- Assert.Null(parser.ParseFile(input));
- }
- }
-}
diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
index 3d3288df6..e15f59e5a 100644
--- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
+++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
@@ -12,12 +12,12 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
<PackageReference Include="FsCheck.Xunit" Version="2.16.4" />
- <PackageReference Include="Moq" Version="4.16.1" />
+ <PackageReference Include="Moq" Version="4.17.2" />
</ItemGroup>
<!-- Code Analyzers-->
diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
index 9f571273f..9d6923d05 100644
--- a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
+++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
@@ -13,8 +13,8 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
- <PackageReference Include="Moq" Version="4.16.1" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
+ <PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/AudioResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/AudioResolverTests.cs
new file mode 100644
index 000000000..aec523882
--- /dev/null
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/AudioResolverTests.cs
@@ -0,0 +1,86 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Naming.Common;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Providers.MediaInfo;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Providers.Tests.MediaInfo;
+
+public class AudioResolverTests
+{
+ private readonly AudioResolver _audioResolver;
+
+ public AudioResolverTests()
+ {
+ // prep BaseItem and Video for calls made that expect managers
+ Video.LiveTvManager = Mock.Of<ILiveTvManager>();
+
+ var applicationPaths = new Mock<IServerApplicationPaths>().Object;
+ var serverConfig = new Mock<IServerConfigurationManager>();
+ serverConfig.Setup(c => c.ApplicationPaths)
+ .Returns(applicationPaths);
+ BaseItem.ConfigurationManager = serverConfig.Object;
+
+ // build resolver to test with
+ var localizationManager = Mock.Of<ILocalizationManager>();
+
+ var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
+ mediaEncoder.Setup(me => me.GetMediaInfo(It.IsAny<MediaInfoRequest>(), It.IsAny<CancellationToken>()))
+ .Returns<MediaInfoRequest, CancellationToken>((_, _) => Task.FromResult(new MediaBrowser.Model.MediaInfo.MediaInfo
+ {
+ MediaStreams = new List<MediaStream>
+ {
+ new()
+ }
+ }));
+
+ var fileSystem = new Mock<IFileSystem>(MockBehavior.Strict);
+ fileSystem.Setup(fs => fs.DirectoryExists(It.IsRegex(MediaInfoResolverTests.VideoDirectoryRegex)))
+ .Returns(true);
+ fileSystem.Setup(fs => fs.DirectoryExists(It.IsRegex(MediaInfoResolverTests.MetadataDirectoryRegex)))
+ .Returns(true);
+
+ _audioResolver = new AudioResolver(localizationManager, mediaEncoder.Object, fileSystem.Object, new NamingOptions());
+ }
+
+ [Theory]
+ [InlineData("My.Video.srt", false, false)]
+ [InlineData("My.Video.mp3", false, true)]
+ [InlineData("My.Video.srt", true, false)]
+ [InlineData("My.Video.mp3", true, true)]
+ public async void GetExternalStreams_MixedFilenames_PicksAudio(string file, bool metadataDirectory, bool matches)
+ {
+ BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
+
+ var video = new Movie
+ {
+ Path = MediaInfoResolverTests.VideoDirectoryPath + "/My.Video.mkv"
+ };
+
+ var directoryService = MediaInfoResolverTests.GetDirectoryServiceForExternalFile(file, metadataDirectory);
+ var streams = await _audioResolver.GetExternalStreamsAsync(video, 0, directoryService, false, CancellationToken.None);
+
+ if (matches)
+ {
+ Assert.Single(streams);
+ var actual = streams[0];
+ Assert.Equal(MediaStreamType.Audio, actual.Type);
+ }
+ else
+ {
+ Assert.Empty(streams);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs
new file mode 100644
index 000000000..98b4a6ccf
--- /dev/null
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs
@@ -0,0 +1,435 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Naming.Common;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Providers.MediaInfo;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Providers.Tests.MediaInfo;
+
+public class MediaInfoResolverTests
+{
+ public const string VideoDirectoryPath = "Test Data/Video";
+ public const string VideoDirectoryRegex = @"Test Data[/\\]Video";
+ public const string MetadataDirectoryPath = "library/00/00000000000000000000000000000000";
+ public const string MetadataDirectoryRegex = @"library.*";
+
+ private readonly ILocalizationManager _localizationManager;
+ private readonly MediaInfoResolver _subtitleResolver;
+
+ public MediaInfoResolverTests()
+ {
+ // prep BaseItem and Video for calls made that expect managers
+ Video.LiveTvManager = Mock.Of<ILiveTvManager>();
+
+ var applicationPaths = new Mock<IServerApplicationPaths>().Object;
+ var serverConfig = new Mock<IServerConfigurationManager>();
+ serverConfig.Setup(c => c.ApplicationPaths)
+ .Returns(applicationPaths);
+ BaseItem.ConfigurationManager = serverConfig.Object;
+
+ // build resolver to test with
+ var englishCultureDto = new CultureDto("English", "English", "en", new[] { "eng" });
+
+ var localizationManager = new Mock<ILocalizationManager>(MockBehavior.Loose);
+ localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"en.*", RegexOptions.IgnoreCase)))
+ .Returns(englishCultureDto);
+ _localizationManager = localizationManager.Object;
+
+ var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
+ mediaEncoder.Setup(me => me.GetMediaInfo(It.IsAny<MediaInfoRequest>(), It.IsAny<CancellationToken>()))
+ .Returns<MediaInfoRequest, CancellationToken>((_, _) => Task.FromResult(new MediaBrowser.Model.MediaInfo.MediaInfo
+ {
+ MediaStreams = new List<MediaStream>
+ {
+ new()
+ }
+ }));
+
+ var fileSystem = new Mock<IFileSystem>(MockBehavior.Strict);
+ fileSystem.Setup(fs => fs.DirectoryExists(It.IsAny<string>()))
+ .Returns(false);
+ fileSystem.Setup(fs => fs.DirectoryExists(It.IsRegex(VideoDirectoryRegex)))
+ .Returns(true);
+ fileSystem.Setup(fs => fs.DirectoryExists(It.IsRegex(MetadataDirectoryRegex)))
+ .Returns(true);
+
+ _subtitleResolver = new SubtitleResolver(_localizationManager, mediaEncoder.Object, fileSystem.Object, new NamingOptions());
+ }
+
+ [Fact]
+ public void GetExternalFiles_BadProtocol_ReturnsNoSubtitles()
+ {
+ // need a media source manager capable of returning something other than file protocol
+ var mediaSourceManager = new Mock<IMediaSourceManager>();
+ mediaSourceManager.Setup(m => m.GetPathProtocol(It.IsRegex(@"http.*")))
+ .Returns(MediaProtocol.Http);
+ BaseItem.MediaSourceManager = mediaSourceManager.Object;
+
+ var video = new Movie
+ {
+ Path = "https://url.com/My.Video.mkv"
+ };
+
+ Assert.Empty(_subtitleResolver.GetExternalFiles(video, Mock.Of<IDirectoryService>(), false));
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void GetExternalFiles_MissingDirectory_DirectoryNotQueried(bool metadataDirectory)
+ {
+ BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
+
+ string containingFolderPath, metadataPath;
+
+ if (metadataDirectory)
+ {
+ containingFolderPath = VideoDirectoryPath;
+ metadataPath = "invalid";
+ }
+ else
+ {
+ containingFolderPath = "invalid";
+ metadataPath = MetadataDirectoryPath;
+ }
+
+ var video = new Mock<Movie>();
+ video.Setup(m => m.Path)
+ .Returns(VideoDirectoryPath + "/My.Video.mkv");
+ video.Setup(m => m.ContainingFolderPath)
+ .Returns(containingFolderPath);
+ video.Setup(m => m.GetInternalMetadataPath())
+ .Returns(metadataPath);
+
+ string pathNotFoundRegex = metadataDirectory ? MetadataDirectoryRegex : VideoDirectoryRegex;
+
+ var directoryService = new Mock<IDirectoryService>(MockBehavior.Strict);
+ // any path other than test target exists and provides an empty listing
+ directoryService.Setup(ds => ds.GetFilePaths(It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<bool>()))
+ .Returns(Array.Empty<string>());
+
+ _subtitleResolver.GetExternalFiles(video.Object, directoryService.Object, false);
+
+ directoryService.Verify(
+ ds => ds.GetFilePaths(It.IsRegex(pathNotFoundRegex), It.IsAny<bool>(), It.IsAny<bool>()),
+ Times.Never);
+ }
+
+ [Theory]
+ [InlineData("My.Video.mkv", "My.Video.srt", null)]
+ [InlineData("My.Video.mkv", "My.Video.en.srt", "eng")]
+ [InlineData("My.Video.mkv", "My.Video.en.srt", "eng", true)]
+ [InlineData("Example Movie (2021).mp4", "Example Movie (2021).English.Srt", "eng")]
+ [InlineData("[LTDB] Who Framed Roger Rabbit (1998) - [Bluray-1080p].mkv", "[LTDB] Who Framed Roger Rabbit (1998) - [Bluray-1080p].en.srt", "eng")]
+ public void GetExternalFiles_NameMatching_MatchesAndParsesToken(string movie, string file, string? language, bool metadataDirectory = false)
+ {
+ BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
+
+ var video = new Movie
+ {
+ Path = VideoDirectoryPath + "/" + movie
+ };
+
+ var directoryService = GetDirectoryServiceForExternalFile(file, metadataDirectory);
+ var streams = _subtitleResolver.GetExternalFiles(video, directoryService, false).ToList();
+
+ Assert.Single(streams);
+ var actual = streams[0];
+ Assert.Equal(language, actual.Language);
+ Assert.Null(actual.Title);
+ }
+
+ [Theory]
+ [InlineData("cover.jpg")]
+ [InlineData("My.Video.mp3")]
+ [InlineData("My.Video.png")]
+ [InlineData("My.Video.txt")]
+ [InlineData("My.Video Sequel.srt")]
+ [InlineData("Some.Other.Video.srt")]
+ public void GetExternalFiles_NameMatching_RejectsNonMatches(string file)
+ {
+ BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
+
+ var video = new Movie
+ {
+ Path = VideoDirectoryPath + "/My.Video.mkv"
+ };
+
+ var directoryService = GetDirectoryServiceForExternalFile(file);
+ var streams = _subtitleResolver.GetExternalFiles(video, directoryService, false).ToList();
+
+ Assert.Empty(streams);
+ }
+
+ [Theory]
+ [InlineData("https://url.com/My.Video.mkv")]
+ [InlineData(VideoDirectoryPath)] // valid but no files found for this test
+ public async void GetExternalStreams_BadPaths_ReturnsNoSubtitles(string path)
+ {
+ // need a media source manager capable of returning something other than file protocol
+ var mediaSourceManager = new Mock<IMediaSourceManager>();
+ mediaSourceManager.Setup(m => m.GetPathProtocol(It.IsRegex(@"http.*")))
+ .Returns(MediaProtocol.Http);
+ BaseItem.MediaSourceManager = mediaSourceManager.Object;
+
+ var video = new Movie
+ {
+ Path = path
+ };
+
+ var directoryService = new Mock<IDirectoryService>(MockBehavior.Strict);
+ directoryService.Setup(ds => ds.GetFilePaths(It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<bool>()))
+ .Returns(Array.Empty<string>());
+
+ var mediaEncoder = Mock.Of<IMediaEncoder>(MockBehavior.Strict);
+ var fileSystem = Mock.Of<IFileSystem>();
+
+ var subtitleResolver = new SubtitleResolver(_localizationManager, mediaEncoder, fileSystem, new NamingOptions());
+
+ var streams = await subtitleResolver.GetExternalStreamsAsync(video, 0, directoryService.Object, false, CancellationToken.None);
+
+ Assert.Empty(streams);
+ }
+
+ private static TheoryData<string, MediaStream[], MediaStream[]> GetExternalStreams_MergeMetadata_HandlesOverridesCorrectly_Data()
+ {
+ var data = new TheoryData<string, MediaStream[], MediaStream[]>();
+
+ // filename and stream have no metadata set
+ string file = "My.Video.srt";
+ data.Add(
+ file,
+ new[]
+ {
+ CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0)
+ },
+ new[]
+ {
+ CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0)
+ });
+
+ // filename has metadata
+ file = "My.Video.Title1.default.forced.en.srt";
+ data.Add(
+ file,
+ new[]
+ {
+ CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0)
+ },
+ new[]
+ {
+ CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title1", 0, true, true)
+ });
+
+ // single stream with metadata
+ file = "My.Video.mks";
+ data.Add(
+ file,
+ new[]
+ {
+ CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, true)
+ },
+ new[]
+ {
+ CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, true)
+ });
+
+ // stream wins for title/language, filename wins for flags when conflicting
+ file = "My.Video.Title2.default.forced.en.srt";
+ data.Add(
+ file,
+ new[]
+ {
+ CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 0)
+ },
+ new[]
+ {
+ CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 0, true, true)
+ });
+
+ // multiple stream with metadata - filename flags ignored but other data filled in when missing from stream
+ file = "My.Video.Title3.default.forced.en.srt";
+ data.Add(
+ file,
+ new[]
+ {
+ CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0, true, true),
+ CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 1)
+ },
+ new[]
+ {
+ CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title3", 0, true, true),
+ CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 1)
+ });
+
+ return data;
+ }
+
+ [Theory]
+ [MemberData(nameof(GetExternalStreams_MergeMetadata_HandlesOverridesCorrectly_Data))]
+ public async void GetExternalStreams_MergeMetadata_HandlesOverridesCorrectly(string file, MediaStream[] inputStreams, MediaStream[] expectedStreams)
+ {
+ BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
+
+ var video = new Movie
+ {
+ Path = VideoDirectoryPath + "/My.Video.mkv"
+ };
+
+ var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
+ mediaEncoder.Setup(me => me.GetMediaInfo(It.IsAny<MediaInfoRequest>(), It.IsAny<CancellationToken>()))
+ .Returns<MediaInfoRequest, CancellationToken>((_, _) => Task.FromResult(new MediaBrowser.Model.MediaInfo.MediaInfo
+ {
+ MediaStreams = inputStreams.ToList()
+ }));
+
+ var fileSystem = new Mock<IFileSystem>(MockBehavior.Strict);
+ fileSystem.Setup(fs => fs.DirectoryExists(It.IsRegex(VideoDirectoryRegex)))
+ .Returns(true);
+ fileSystem.Setup(fs => fs.DirectoryExists(It.IsRegex(MetadataDirectoryRegex)))
+ .Returns(true);
+
+ var subtitleResolver = new SubtitleResolver(_localizationManager, mediaEncoder.Object, fileSystem.Object, new NamingOptions());
+
+ var directoryService = GetDirectoryServiceForExternalFile(file);
+ var streams = await subtitleResolver.GetExternalStreamsAsync(video, 0, directoryService, false, CancellationToken.None);
+
+ Assert.Equal(expectedStreams.Length, streams.Count);
+ for (var i = 0; i < expectedStreams.Length; i++)
+ {
+ var expected = expectedStreams[i];
+ var actual = streams[i];
+
+ Assert.True(actual.IsExternal);
+ Assert.Equal(expected.Index, actual.Index);
+ Assert.Equal(expected.Type, actual.Type);
+ Assert.Equal(expected.Path, actual.Path);
+ Assert.Equal(expected.IsDefault, actual.IsDefault);
+ Assert.Equal(expected.IsForced, actual.IsForced);
+ Assert.Equal(expected.Language, actual.Language);
+ Assert.Equal(expected.Title, actual.Title);
+ }
+ }
+
+ [Theory]
+ [InlineData(1, 1)]
+ [InlineData(1, 2)]
+ [InlineData(2, 1)]
+ [InlineData(2, 2)]
+ public async void GetExternalStreams_StreamIndex_HandlesFilesAndContainers(int fileCount, int streamCount)
+ {
+ BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
+
+ var video = new Movie
+ {
+ Path = VideoDirectoryPath + "/My.Video.mkv"
+ };
+
+ var files = new string[fileCount];
+ for (int i = 0; i < fileCount; i++)
+ {
+ files[i] = $"{VideoDirectoryPath}/My.Video.{i}.srt";
+ }
+
+ var directoryService = new Mock<IDirectoryService>(MockBehavior.Strict);
+ directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(VideoDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>()))
+ .Returns(files);
+ directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(MetadataDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>()))
+ .Returns(Array.Empty<string>());
+
+ List<MediaStream> GenerateMediaStreams()
+ {
+ var mediaStreams = new List<MediaStream>();
+ for (int i = 0; i < streamCount; i++)
+ {
+ mediaStreams.Add(new());
+ }
+
+ return mediaStreams;
+ }
+
+ var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
+ mediaEncoder.Setup(me => me.GetMediaInfo(It.IsAny<MediaInfoRequest>(), It.IsAny<CancellationToken>()))
+ .Returns<MediaInfoRequest, CancellationToken>((_, _) => Task.FromResult(new MediaBrowser.Model.MediaInfo.MediaInfo
+ {
+ MediaStreams = GenerateMediaStreams()
+ }));
+
+ var fileSystem = new Mock<IFileSystem>(MockBehavior.Strict);
+ fileSystem.Setup(fs => fs.DirectoryExists(It.IsRegex(VideoDirectoryRegex)))
+ .Returns(true);
+ fileSystem.Setup(fs => fs.DirectoryExists(It.IsRegex(MetadataDirectoryRegex)))
+ .Returns(true);
+
+ var subtitleResolver = new SubtitleResolver(_localizationManager, mediaEncoder.Object, fileSystem.Object, new NamingOptions());
+
+ int startIndex = 1;
+ var streams = await subtitleResolver.GetExternalStreamsAsync(video, startIndex, directoryService.Object, false, CancellationToken.None);
+
+ Assert.Equal(fileCount * streamCount, streams.Count);
+ for (var i = 0; i < streams.Count; i++)
+ {
+ Assert.Equal(startIndex + i, streams[i].Index);
+ // intentional integer division to ensure correct number of streams come back from each file
+ Assert.Matches(@$".*\.{i / streamCount}\.srt", streams[i].Path);
+ }
+ }
+
+ private static MediaStream CreateMediaStream(string path, string? language, string? title, int index, bool isForced = false, bool isDefault = false)
+ {
+ return new MediaStream
+ {
+ Index = index,
+ Type = MediaStreamType.Subtitle,
+ Path = path,
+ IsDefault = isDefault,
+ IsForced = isForced,
+ Language = language,
+ Title = title
+ };
+ }
+
+ /// <summary>
+ /// Provides an <see cref="IDirectoryService"/> that when queried for the test video/metadata directory will return a path including the provided file name.
+ /// </summary>
+ /// <param name="file">The name of the file to locate.</param>
+ /// <param name="useMetadataDirectory"><c>true</c> if the file belongs in the metadata directory.</param>
+ /// <returns>A mocked <see cref="IDirectoryService"/>.</returns>
+ public static IDirectoryService GetDirectoryServiceForExternalFile(string file, bool useMetadataDirectory = false)
+ {
+ var directoryService = new Mock<IDirectoryService>(MockBehavior.Strict);
+ if (useMetadataDirectory)
+ {
+ directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(VideoDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>()))
+ .Returns(Array.Empty<string>());
+ directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(MetadataDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>()))
+ .Returns(new[] { MetadataDirectoryPath + "/" + file });
+ }
+ else
+ {
+ directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(VideoDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>()))
+ .Returns(new[] { VideoDirectoryPath + "/" + file });
+ directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(MetadataDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>()))
+ .Returns(Array.Empty<string>());
+ }
+
+ return directoryService.Object;
+ }
+}
diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs
index 040ea5d1d..0e6457ce3 100644
--- a/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs
@@ -1,129 +1,86 @@
-#pragma warning disable CA1002 // Do not expose generic lists
-
using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Naming.Common;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.IO;
using MediaBrowser.Providers.MediaInfo;
using Moq;
using Xunit;
-namespace Jellyfin.Providers.Tests.MediaInfo
+namespace Jellyfin.Providers.Tests.MediaInfo;
+
+public class SubtitleResolverTests
{
- public class SubtitleResolverTests
- {
- public static TheoryData<List<MediaStream>, string, int, string[], MediaStream[]> AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles_TestData()
- {
- var data = new TheoryData<List<MediaStream>, string, int, string[], MediaStream[]>();
+ private readonly SubtitleResolver _subtitleResolver;
- var index = 0;
- data.Add(
- new List<MediaStream>(),
- "/video/My.Video.mkv",
- index,
- new[]
- {
- "/video/My.Video.mp3",
- "/video/My.Video.png",
- "/video/My.Video.srt",
- "/video/My.Video.txt",
- "/video/My.Video.vtt",
- "/video/My.Video.ass",
- "/video/My.Video.sub",
- "/video/My.Video.ssa",
- "/video/My.Video.smi",
- "/video/My.Video.sami",
- "/video/My.Video.en.srt",
- "/video/My.Video.default.en.srt",
- "/video/My.Video.default.forced.en.srt",
- "/video/My.Video.en.default.forced.srt",
- "/video/My.Video.With.Additional.Garbage.en.srt",
- "/video/My.Video With Additional Garbage.srt"
- },
- new[]
- {
- CreateMediaStream("/video/My.Video.srt", "srt", null, index++),
- CreateMediaStream("/video/My.Video.vtt", "vtt", null, index++),
- CreateMediaStream("/video/My.Video.ass", "ass", null, index++),
- CreateMediaStream("/video/My.Video.sub", "sub", null, index++),
- CreateMediaStream("/video/My.Video.ssa", "ssa", null, index++),
- CreateMediaStream("/video/My.Video.smi", "smi", null, index++),
- CreateMediaStream("/video/My.Video.sami", "sami", null, index++),
- CreateMediaStream("/video/My.Video.en.srt", "srt", "en", index++),
- CreateMediaStream("/video/My.Video.default.en.srt", "srt", "en", index++, isDefault: true),
- CreateMediaStream("/video/My.Video.default.forced.en.srt", "srt", "en", index++, isForced: true, isDefault: true),
- CreateMediaStream("/video/My.Video.en.default.forced.srt", "srt", "en", index++, isForced: true, isDefault: true),
- CreateMediaStream("/video/My.Video.With.Additional.Garbage.en.srt", "srt", "en", index),
- });
+ public SubtitleResolverTests()
+ {
+ // prep BaseItem and Video for calls made that expect managers
+ Video.LiveTvManager = Mock.Of<ILiveTvManager>();
- return data;
- }
+ var applicationPaths = new Mock<IServerApplicationPaths>().Object;
+ var serverConfig = new Mock<IServerConfigurationManager>();
+ serverConfig.Setup(c => c.ApplicationPaths)
+ .Returns(applicationPaths);
+ BaseItem.ConfigurationManager = serverConfig.Object;
- [Theory]
- [MemberData(nameof(AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles_TestData))]
- public void AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles(List<MediaStream> streams, string videoPath, int startIndex, string[] files, MediaStream[] expectedResult)
- {
- new SubtitleResolver(Mock.Of<ILocalizationManager>()).AddExternalSubtitleStreams(streams, videoPath, startIndex, files);
+ // build resolver to test with
+ var localizationManager = Mock.Of<ILocalizationManager>();
- Assert.Equal(expectedResult.Length, streams.Count);
- for (var i = 0; i < expectedResult.Length; i++)
+ var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
+ mediaEncoder.Setup(me => me.GetMediaInfo(It.IsAny<MediaInfoRequest>(), It.IsAny<CancellationToken>()))
+ .Returns<MediaInfoRequest, CancellationToken>((_, _) => Task.FromResult(new MediaBrowser.Model.MediaInfo.MediaInfo
{
- var expected = expectedResult[i];
- var actual = streams[i];
+ MediaStreams = new List<MediaStream>
+ {
+ new()
+ }
+ }));
- Assert.Equal(expected.Index, actual.Index);
- Assert.Equal(expected.Type, actual.Type);
- Assert.Equal(expected.IsExternal, actual.IsExternal);
- Assert.Equal(expected.Path, actual.Path);
- Assert.Equal(expected.IsDefault, actual.IsDefault);
- Assert.Equal(expected.IsForced, actual.IsForced);
- Assert.Equal(expected.Language, actual.Language);
- }
- }
+ var fileSystem = new Mock<IFileSystem>(MockBehavior.Strict);
+ fileSystem.Setup(fs => fs.DirectoryExists(It.IsRegex(MediaInfoResolverTests.VideoDirectoryRegex)))
+ .Returns(true);
+ fileSystem.Setup(fs => fs.DirectoryExists(It.IsRegex(MediaInfoResolverTests.MetadataDirectoryRegex)))
+ .Returns(true);
+
+ _subtitleResolver = new SubtitleResolver(localizationManager, mediaEncoder.Object, fileSystem.Object, new NamingOptions());
+ }
+
+ [Theory]
+ [InlineData("My.Video.srt", false, true)]
+ [InlineData("My.Video.mp3", false, false)]
+ [InlineData("My.Video.srt", true, true)]
+ [InlineData("My.Video.mp3", true, false)]
+ public async void GetExternalStreams_MixedFilenames_PicksSubtitles(string file, bool metadataDirectory, bool matches)
+ {
+ BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
- [Theory]
- [InlineData("/video/My Video.mkv", "/video/My Video.srt", "srt", null, false, false)]
- [InlineData("/video/My.Video.mkv", "/video/My.Video.srt", "srt", null, false, false)]
- [InlineData("/video/My.Video.mkv", "/video/My.Video.foreign.srt", "srt", null, true, false)]
- [InlineData("/video/My Video.mkv", "/video/My Video.forced.srt", "srt", null, true, false)]
- [InlineData("/video/My.Video.mkv", "/video/My.Video.default.srt", "srt", null, false, true)]
- [InlineData("/video/My.Video.mkv", "/video/My.Video.forced.default.srt", "srt", null, true, true)]
- [InlineData("/video/My.Video.mkv", "/video/My.Video.en.srt", "srt", "en", false, false)]
- [InlineData("/video/My.Video.mkv", "/video/My.Video.default.en.srt", "srt", "en", false, true)]
- [InlineData("/video/My.Video.mkv", "/video/My.Video.default.forced.en.srt", "srt", "en", true, true)]
- [InlineData("/video/My.Video.mkv", "/video/My.Video.en.default.forced.srt", "srt", "en", true, true)]
- public void AddExternalSubtitleStreams_GivenSingleFile_ReturnsExpectedSubtitle(string videoPath, string file, string codec, string? language, bool isForced, bool isDefault)
+ var video = new Movie
{
- var streams = new List<MediaStream>();
- var expected = CreateMediaStream(file, codec, language, 0, isForced, isDefault);
+ Path = MediaInfoResolverTests.VideoDirectoryPath + "/My.Video.mkv"
+ };
- new SubtitleResolver(Mock.Of<ILocalizationManager>()).AddExternalSubtitleStreams(streams, videoPath, 0, new[] { file });
+ var directoryService = MediaInfoResolverTests.GetDirectoryServiceForExternalFile(file, metadataDirectory);
+ var streams = await _subtitleResolver.GetExternalStreamsAsync(video, 0, directoryService, false, CancellationToken.None);
+ if (matches)
+ {
Assert.Single(streams);
-
var actual = streams[0];
-
- Assert.Equal(expected.Index, actual.Index);
- Assert.Equal(expected.Type, actual.Type);
- Assert.Equal(expected.IsExternal, actual.IsExternal);
- Assert.Equal(expected.Path, actual.Path);
- Assert.Equal(expected.IsDefault, actual.IsDefault);
- Assert.Equal(expected.IsForced, actual.IsForced);
- Assert.Equal(expected.Language, actual.Language);
+ Assert.Equal(MediaStreamType.Subtitle, actual.Type);
}
-
- private static MediaStream CreateMediaStream(string path, string codec, string? language, int index, bool isForced = false, bool isDefault = false)
+ else
{
- return new()
- {
- Index = index,
- Codec = codec,
- Type = MediaStreamType.Subtitle,
- IsExternal = true,
- Path = path,
- IsDefault = isDefault,
- IsForced = isForced,
- Language = language
- };
+ Assert.Empty(streams);
}
}
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
index 3146f277f..55920c928 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -21,8 +21,8 @@
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
- <PackageReference Include="Moq" Version="4.16.1" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
+ <PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="Xunit.SkippableFact" Version="1.4.13" />
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
index 5c7c983c2..c21871297 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
@@ -1,4 +1,4 @@
-using Emby.Naming.Common;
+using Emby.Naming.Common;
using Emby.Server.Implementations.Library.Resolvers.TV;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
@@ -7,6 +7,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
+using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
@@ -21,7 +22,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
{
var parent = new Folder { Name = "extras" };
- var episodeResolver = new EpisodeResolver(_namingOptions);
+ var episodeResolver = new EpisodeResolver(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions);
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
Mock.Of<IDirectoryService>())
@@ -44,7 +45,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
// Have to create a mock because of moq proxies not being castable to a concrete implementation
// https://github.com/jellyfin/jellyfin/blob/ab0cff8556403e123642dc9717ba778329554634/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs#L48
- var episodeResolver = new EpisodeResolverMock(_namingOptions);
+ var episodeResolver = new EpisodeResolverMock(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions);
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
Mock.Of<IDirectoryService>())
@@ -61,7 +62,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
private class EpisodeResolverMock : EpisodeResolver
{
- public EpisodeResolverMock(NamingOptions namingOptions) : base(namingOptions)
+ public EpisodeResolverMock(ILogger<EpisodeResolver> logger, NamingOptions namingOptions) : base(logger, namingOptions)
{
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs
index f5c8cc970..599599071 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs
index f2efcddba..efc3ac0c2 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs
@@ -5,6 +5,7 @@ using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
+using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
@@ -17,7 +18,7 @@ public class MovieResolverTests
[Fact]
public void Resolve_GivenLocalAlternateVersion_ResolvesToVideo()
{
- var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), _namingOptions);
+ var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions);
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
Mock.Of<IDirectoryService>())
diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
index 08eea4b15..55101ce10d 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
+++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
@@ -9,14 +9,14 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.2" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.3" />
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="Xunit.Priority" Version="1.1.6" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
- <PackageReference Include="Moq" Version="4.16.1" />
+ <PackageReference Include="Moq" Version="4.17.2" />
</ItemGroup>
<ItemGroup>
diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
index 2ab32d6f6..f5d60d2d3 100644
--- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
+++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
@@ -10,13 +10,13 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.2" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.3" />
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
- <PackageReference Include="Moq" Version="4.16.1" />
+ <PackageReference Include="Moq" Version="4.17.2" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
index 1bb2115cc..7689d1da3 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
@@ -13,8 +13,8 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
- <PackageReference Include="Moq" Version="4.16.1" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
+ <PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />