aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.config/dotnet-tools.json2
-rw-r--r--.github/ISSUE_TEMPLATE/issue report.yml2
-rw-r--r--.github/workflows/ci-codeql-analysis.yml6
-rw-r--r--.github/workflows/ci-openapi.yml4
-rw-r--r--.github/workflows/ci-tests.yml2
-rw-r--r--.github/workflows/commands.yml2
-rw-r--r--.github/workflows/issue-template-check.yml2
-rw-r--r--Directory.Packages.props29
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs11
-rw-r--r--Emby.Server.Implementations/ConfigurationOptions.cs2
-rw-r--r--Emby.Server.Implementations/Data/BaseSqliteRepository.cs2
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs14
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs2
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs19
-rw-r--r--Emby.Server.Implementations/Localization/Core/ar.json9
-rw-r--r--Emby.Server.Implementations/Localization/Core/cs.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-GB.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-US.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/enm.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-AR.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/es_419.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/km.json132
-rw-r--r--Emby.Server.Implementations/Localization/Core/kw.json135
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/pl.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ro.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/sv.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/uk.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/Session/SessionManager.cs30
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs20
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs8
-rw-r--r--Jellyfin.Api/Controllers/DevicesController.cs16
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs34
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs15
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs18
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs10
-rw-r--r--Jellyfin.Api/Controllers/MediaSegmentsController.cs61
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs2
-rw-r--r--Jellyfin.Api/Controllers/TrickplayController.cs1
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs13
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs23
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs2
-rw-r--r--Jellyfin.Api/Controllers/UserViewsController.cs6
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs8
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs11
-rw-r--r--Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs4
-rw-r--r--Jellyfin.Api/Helpers/RequestHelpers.cs13
-rw-r--r--Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs5
-rw-r--r--Jellyfin.Data/Entities/MediaSegment.cs42
-rw-r--r--Jellyfin.Data/Enums/MediaSegmentType.cs39
-rw-r--r--Jellyfin.Server.Implementations/Devices/DeviceManager.cs157
-rw-r--r--Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs20
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj1
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDbContext.cs5
-rw-r--r--Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs106
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20240729140605_AddMediaSegments.Designer.cs708
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20240729140605_AddMediaSegments.cs38
-rw-r--r--Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs30
-rw-r--r--Jellyfin.Server.Implementations/Security/AuthorizationContext.cs15
-rw-r--r--Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs4
-rw-r--r--Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs2
-rw-r--r--Jellyfin.Server/Startup.cs7
-rw-r--r--MediaBrowser.Controller/Devices/IDeviceManager.cs12
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs13
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs32
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs1
-rw-r--r--MediaBrowser.Controller/Entities/UserRootFolder.cs2
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs2
-rw-r--r--MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs28
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs646
-rw-r--r--MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs7
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs16
-rw-r--r--MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs53
-rw-r--r--MediaBrowser.Controller/Providers/SeasonInfo.cs3
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs3
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs21
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs119
-rw-r--r--MediaBrowser.MediaEncoding/Probing/MediaStreamInfoSideData.cs7
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs6
-rw-r--r--MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs6
-rw-r--r--MediaBrowser.Model/Configuration/LibraryOptions.cs6
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs18
-rw-r--r--MediaBrowser.Model/Dto/MediaSourceInfo.cs2
-rw-r--r--MediaBrowser.Model/Entities/MediaStream.cs6
-rw-r--r--MediaBrowser.Model/Library/UserViewQuery.cs7
-rw-r--r--MediaBrowser.Model/MediaSegments/MediaSegmentDto.cs35
-rw-r--r--MediaBrowser.Model/Plugins/PluginPageInfo.cs4
-rw-r--r--MediaBrowser.Model/Querying/LatestItemsQuery.cs3
-rw-r--r--MediaBrowser.Model/Querying/NextUpQuery.cs26
-rw-r--r--MediaBrowser.Providers/Lyric/LrcLyricParser.cs125
-rw-r--r--MediaBrowser.Providers/Lyric/LyricScheduledTask.cs170
-rw-r--r--MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs12
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs10
-rw-r--r--README.md12
-rw-r--r--src/Jellyfin.Drawing/ImageProcessor.cs3
-rw-r--r--src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs2
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs2
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs12
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs1
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_metadata.json4
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/LiveTvControllerTests.cs96
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/PluginsControllerTests.cs45
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj3
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs2
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Test Data/dummy.m3u81
114 files changed, 2626 insertions, 892 deletions
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index af5264279..a3847dcdf 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "8.0.7",
+ "version": "8.0.8",
"commands": [
"dotnet-ef"
]
diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml
index 85590c0b0..cfb5a6ec2 100644
--- a/.github/ISSUE_TEMPLATE/issue report.yml
+++ b/.github/ISSUE_TEMPLATE/issue report.yml
@@ -86,7 +86,7 @@ body:
label: Jellyfin Server version
description: What version of Jellyfin are you using?
options:
- - 10.9.8+
+ - 10.9.10+
- Master
- Unstable
- Older*
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index ba66526e0..513139ea5 100644
--- a/.github/workflows/ci-codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -27,11 +27,11 @@ jobs:
dotnet-version: '8.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
+ uses: github/codeql-action/init@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
+ uses: github/codeql-action/autobuild@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
+ uses: github/codeql-action/analyze@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml
index d5dc1a860..d01c506db 100644
--- a/.github/workflows/ci-openapi.yml
+++ b/.github/workflows/ci-openapi.yml
@@ -27,7 +27,7 @@ jobs:
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: openapi-head
retention-days: 14
@@ -61,7 +61,7 @@ jobs:
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: openapi-base
retention-days: 14
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index 91c2be87b..af8106c0a 100644
--- a/.github/workflows/ci-tests.yml
+++ b/.github/workflows/ci-tests.yml
@@ -34,7 +34,7 @@ jobs:
--verbosity minimal
- name: Merge code coverage results
- uses: danielpalme/ReportGenerator-GitHub-Action@5808021ec4deecb0ab3da051d49b4ce65fcc20af # 5.3.8
+ uses: danielpalme/ReportGenerator-GitHub-Action@e3af7259842d9c814021ea121f85526e0872b25f # v5.3.9
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index b79185855..4b469f0d7 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -132,7 +132,7 @@ jobs:
with:
repository: jellyfin/jellyfin-triage-script
- name: install python
- uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1
+ uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0
with:
python-version: '3.12'
cache: 'pip'
diff --git a/.github/workflows/issue-template-check.yml b/.github/workflows/issue-template-check.yml
index 6172455c2..d89076595 100644
--- a/.github/workflows/issue-template-check.yml
+++ b/.github/workflows/issue-template-check.yml
@@ -14,7 +14,7 @@ jobs:
with:
repository: jellyfin/jellyfin-triage-script
- name: install python
- uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1
+ uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0
with:
python-version: '3.12'
cache: 'pip'
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 683f2eee3..633c3cabd 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,7 +4,7 @@
</PropertyGroup>
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
<ItemGroup Label="Package Dependencies">
- <PackageVersion Include="AsyncKeyedLock" Version="7.0.0" />
+ <PackageVersion Include="AsyncKeyedLock" Version="7.0.1" />
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
@@ -16,23 +16,22 @@
<PackageVersion Include="Diacritics" Version="3.3.29" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
- <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.5.0" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.2" />
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="4.0.7" />
- <PackageVersion Include="LrcParser" Version="2023.524.0" />
+ <PackageVersion Include="LrcParser" Version="2024.0728.2" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
- <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.7" />
- <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.7" />
+ <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.8" />
+ <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.8" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
- <PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.7" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.7" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.7" />
+ <PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.8" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.8" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.8" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
@@ -41,14 +40,14 @@
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.7" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.7" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.8" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
- <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
+ <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
<PackageVersion Include="MimeTypes" Version="2.4.0" />
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
<PackageVersion Include="Moq" Version="4.18.4" />
@@ -72,7 +71,7 @@
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.8" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
- <PackageVersion Include="Svg.Skia" Version="2.0.0" />
+ <PackageVersion Include="Svg.Skia" Version="2.0.0.1" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" />
@@ -81,7 +80,7 @@
<PackageVersion Include="System.Text.Json" Version="8.0.4" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
- <PackageVersion Include="z440.atl.core" Version="5.25.0" />
+ <PackageVersion Include="z440.atl.core" Version="6.2.0" />
<PackageVersion Include="TMDbLib" Version="2.2.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 5bf9c4fc2..5292003f0 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -40,6 +40,7 @@ using Jellyfin.MediaEncoding.Hls.Playlist;
using Jellyfin.Networking.Manager;
using Jellyfin.Networking.Udp;
using Jellyfin.Server.Implementations;
+using Jellyfin.Server.Implementations.MediaSegments;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
@@ -401,7 +402,12 @@ namespace Emby.Server.Implementations
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
- Resolve<IMediaEncoder>().SetFFmpegPath();
+ var ffmpegValid = Resolve<IMediaEncoder>().SetFFmpegPath();
+
+ if (!ffmpegValid)
+ {
+ throw new FfmpegException("Failed to find valid ffmpeg");
+ }
Logger.LogInformation("ServerId: {ServerId}", SystemId);
Logger.LogInformation("Core startup complete");
@@ -552,6 +558,8 @@ namespace Emby.Server.Implementations
serviceCollection.AddScoped<DynamicHlsHelper>();
serviceCollection.AddScoped<IClientEventLogger, ClientEventLogger>();
serviceCollection.AddSingleton<IDirectoryService, DirectoryService>();
+
+ serviceCollection.AddSingleton<IMediaSegmentManager, MediaSegmentManager>();
}
/// <summary>
@@ -635,6 +643,7 @@ namespace Emby.Server.Implementations
UserView.TVSeriesManager = Resolve<ITVSeriesManager>();
UserView.CollectionManager = Resolve<ICollectionManager>();
BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
+ BaseItem.MediaSegmentManager = Resolve<IMediaSegmentManager>();
CollectionFolder.XmlSerializer = _xmlSerializer;
CollectionFolder.ApplicationHost = this;
}
diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs
index c06cd8510..e86010513 100644
--- a/Emby.Server.Implementations/ConfigurationOptions.cs
+++ b/Emby.Server.Implementations/ConfigurationOptions.cs
@@ -20,7 +20,7 @@ namespace Emby.Server.Implementations
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
{ BindToUnixSocketKey, bool.FalseString },
{ SqliteCacheSizeKey, "20000" },
- { SqliteDisableSecondLevelCacheKey, bool.FalseString }
+ { FfmpegSkipValidationKey, bool.FalseString }
};
}
}
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
index 5291999dc..8ed72c208 100644
--- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
+++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
@@ -56,7 +56,7 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Gets the journal size limit. <see href="https://www.sqlite.org/pragma.html#pragma_journal_size_limit" />.
- /// The default (-1) is overriden to prevent unconstrained WAL size, as reported by users.
+ /// The default (-1) is overridden to prevent unconstrained WAL size, as reported by users.
/// </summary>
/// <value>The journal size limit.</value>
protected virtual int? JournalSizeLimit => 134_217_728; // 128MiB
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 5094dcf0d..60f5ee47a 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -183,7 +183,8 @@ namespace Emby.Server.Implementations.Data
"ElPresentFlag",
"BlPresentFlag",
"DvBlSignalCompatibilityId",
- "IsHearingImpaired"
+ "IsHearingImpaired",
+ "Rotation"
};
private static readonly string _mediaStreamSaveColumnsInsertQuery =
@@ -343,7 +344,7 @@ namespace Emby.Server.Implementations.Data
base.Initialize();
const string CreateMediaStreamsTableCommand
- = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, IsHearingImpaired BIT NULL, PRIMARY KEY (ItemId, StreamIndex))";
+ = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, IsHearingImpaired BIT NULL, Rotation INT NULL, PRIMARY KEY (ItemId, StreamIndex))";
const string CreateMediaAttachmentsTableCommand
= "create table if not exists mediaattachments (ItemId GUID, AttachmentIndex INT, Codec TEXT, CodecTag TEXT NULL, Comment TEXT NULL, Filename TEXT NULL, MIMEType TEXT NULL, PRIMARY KEY (ItemId, AttachmentIndex))";
@@ -538,6 +539,8 @@ namespace Emby.Server.Implementations.Data
AddColumn(connection, "MediaStreams", "IsHearingImpaired", "BIT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "Rotation", "INT", existingColumnNames);
+
connection.Execute(string.Join(';', postQueries));
transaction.Commit();
@@ -5483,6 +5486,8 @@ AND Type = @InternalPersonType)");
statement.TryBind("@DvBlSignalCompatibilityId" + index, stream.DvBlSignalCompatibilityId);
statement.TryBind("@IsHearingImpaired" + index, stream.IsHearingImpaired);
+
+ statement.TryBind("@Rotation" + index, stream.Rotation);
}
statement.ExecuteNonQuery();
@@ -5694,6 +5699,11 @@ AND Type = @InternalPersonType)");
item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result;
+ if (reader.TryGetInt32(44, out var rotation))
+ {
+ item.Rotation = rotation;
+ }
+
if (item.Type is MediaStreamType.Audio or MediaStreamType.Subtitle)
{
item.LocalizedDefault = _localization.GetLocalizedString("Default");
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index cbded1ec6..6d71e99a1 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -1530,7 +1530,7 @@ namespace Emby.Server.Implementations.Library
{
var userViews = UserViewManager.GetUserViews(new UserViewQuery
{
- UserId = user.Id,
+ User = user,
IncludeHidden = true,
IncludeExternalContent = allowExternalContent
});
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index d9a559014..e9cf47d46 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -16,7 +16,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Channels;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
@@ -27,17 +26,15 @@ namespace Emby.Server.Implementations.Library
{
private readonly ILibraryManager _libraryManager;
private readonly ILocalizationManager _localizationManager;
- private readonly IUserManager _userManager;
private readonly IChannelManager _channelManager;
private readonly ILiveTvManager _liveTvManager;
private readonly IServerConfigurationManager _config;
- public UserViewManager(ILibraryManager libraryManager, ILocalizationManager localizationManager, IUserManager userManager, IChannelManager channelManager, ILiveTvManager liveTvManager, IServerConfigurationManager config)
+ public UserViewManager(ILibraryManager libraryManager, ILocalizationManager localizationManager, IChannelManager channelManager, ILiveTvManager liveTvManager, IServerConfigurationManager config)
{
_libraryManager = libraryManager;
_localizationManager = localizationManager;
- _userManager = userManager;
_channelManager = channelManager;
_liveTvManager = liveTvManager;
_config = config;
@@ -45,11 +42,7 @@ namespace Emby.Server.Implementations.Library
public Folder[] GetUserViews(UserViewQuery query)
{
- var user = _userManager.GetUserById(query.UserId);
- if (user is null)
- {
- throw new ArgumentException("User id specified in the query does not exist.", nameof(query));
- }
+ var user = query.User;
var folders = _libraryManager.GetUserRootFolder()
.GetChildren(user, true)
@@ -125,14 +118,14 @@ namespace Emby.Server.Implementations.Library
{
var channelResult = _channelManager.GetChannelsInternalAsync(new ChannelQuery
{
- UserId = query.UserId
+ UserId = user.Id
}).GetAwaiter().GetResult();
var channels = channelResult.Items;
list.AddRange(channels);
- if (_liveTvManager.GetEnabledUsers().Select(i => i.Id).Contains(query.UserId))
+ if (_liveTvManager.GetEnabledUsers().Select(i => i.Id).Contains(user.Id))
{
list.Add(_liveTvManager.GetInternalLiveTvFolder(CancellationToken.None));
}
@@ -207,9 +200,7 @@ namespace Emby.Server.Implementations.Library
public List<Tuple<BaseItem, List<BaseItem>>> GetLatestItems(LatestItemsQuery request, DtoOptions options)
{
- var user = _userManager.GetUserById(request.UserId);
-
- var libraryItems = GetItemsForLatestItems(user, request, options);
+ var libraryItems = GetItemsForLatestItems(request.User, request, options);
var list = new List<Tuple<BaseItem, List<BaseItem>>>();
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index 4245656ff..bd45b0b96 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -1,14 +1,14 @@
{
- "Albums": "البومات",
+ "Albums": "ألبومات",
"AppDeviceValues": "تطبيق: {0}, جهاز: {1}",
"Application": "تطبيق",
- "Artists": "الفنانين",
+ "Artists": "الفنانون",
"AuthenticationSucceededWithUserName": "نجحت عملية التوثيق بـ {0}",
"Books": "الكتب",
"CameraImageUploadedFrom": "رُفعت صورة الكاميرا الجديدة من {0}",
"Channels": "القنوات",
"ChapterNameValue": "الفصل {0}",
- "Collections": "التجميعات",
+ "Collections": "المجموعات",
"DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
"DeviceOnlineWithName": "{0} متصل",
"FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فاشلة من {0}",
@@ -130,5 +130,6 @@
"TaskCleanCollectionsAndPlaylists": "حذف المجموعات وقوائم التشغيل",
"TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة.",
"TaskAudioNormalization": "تطبيع الصوت",
- "TaskAudioNormalizationDescription": "مسح الملفات لتطبيع بيانات الصوت."
+ "TaskAudioNormalizationDescription": "مسح الملفات لتطبيع بيانات الصوت.",
+ "TaskDownloadMissingLyrics": "تنزيل عبارات القصيدة"
}
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index 14cfeb71a..ad9e555a3 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -130,5 +130,7 @@
"TaskCleanCollectionsAndPlaylists": "Pročistit kolekce a seznamy přehrávání",
"TaskCleanCollectionsAndPlaylistsDescription": "Odstraní neexistující položky z kolekcí a seznamů přehrávání.",
"TaskAudioNormalization": "Normalizace zvuku",
- "TaskAudioNormalizationDescription": "Skenovat soubory za účelem normalizace zvuku."
+ "TaskAudioNormalizationDescription": "Skenovat soubory za účelem normalizace zvuku.",
+ "TaskDownloadMissingLyrics": "Stáhnout chybějící texty k písni",
+ "TaskDownloadMissingLyricsDescription": "Stáhne texty k písni"
}
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index ce98979e6..865a1ef95 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -130,5 +130,7 @@
"TaskCleanCollectionsAndPlaylists": "Sammlungen und Playlisten aufräumen",
"TaskCleanCollectionsAndPlaylistsDescription": "Lösche nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten.",
"TaskAudioNormalization": "Audio Normalisierung",
- "TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten."
+ "TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten.",
+ "TaskDownloadMissingLyricsDescription": "Lädt Liedtexte herunter",
+ "TaskDownloadMissingLyrics": "Fehlende Liedtexte herunterladen"
}
diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json
index 75285fe8e..65df1e45b 100644
--- a/Emby.Server.Implementations/Localization/Core/en-GB.json
+++ b/Emby.Server.Implementations/Localization/Core/en-GB.json
@@ -130,5 +130,7 @@
"TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists",
"TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist.",
"TaskAudioNormalization": "Audio Normalisation",
- "TaskAudioNormalizationDescription": "Scans files for audio normalisation data."
+ "TaskAudioNormalizationDescription": "Scans files for audio normalisation data.",
+ "TaskDownloadMissingLyrics": "Download missing lyrics",
+ "TaskDownloadMissingLyricsDescription": "Downloads lyrics for songs"
}
diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json
index 1a69627fa..d1410ef5e 100644
--- a/Emby.Server.Implementations/Localization/Core/en-US.json
+++ b/Emby.Server.Implementations/Localization/Core/en-US.json
@@ -122,6 +122,8 @@
"TaskCleanTranscodeDescription": "Deletes transcode files more than one day old.",
"TaskRefreshChannels": "Refresh Channels",
"TaskRefreshChannelsDescription": "Refreshes internet channel information.",
+ "TaskDownloadMissingLyrics": "Download missing lyrics",
+ "TaskDownloadMissingLyricsDescription": "Downloads lyrics for songs",
"TaskDownloadMissingSubtitles": "Download missing subtitles",
"TaskDownloadMissingSubtitlesDescription": "Searches the internet for missing subtitles based on metadata configuration.",
"TaskOptimizeDatabase": "Optimize database",
diff --git a/Emby.Server.Implementations/Localization/Core/enm.json b/Emby.Server.Implementations/Localization/Core/enm.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/enm.json
@@ -0,0 +1 @@
+{}
diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json
index 8bd3c5def..b926d9d30 100644
--- a/Emby.Server.Implementations/Localization/Core/es-AR.json
+++ b/Emby.Server.Implementations/Localization/Core/es-AR.json
@@ -124,5 +124,13 @@
"External": "Externo",
"TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reprodución HLS más precisas. Esta tarea puede durar mucho tiempo.",
"TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
- "HearingImpaired": "Discapacidad Auditiva"
+ "HearingImpaired": "Discapacidad Auditiva",
+ "TaskRefreshTrickplayImages": "Generar imágenes de Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Crea vistas previas de reproducción engañosa para videos en bibliotecas habilitadas.",
+ "TaskAudioNormalization": "Normalización de audio",
+ "TaskAudioNormalizationDescription": "Escanea archivos en busca de datos de normalización de audio.",
+ "TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen.",
+ "TaskDownloadMissingLyrics": "Descargar letra faltante",
+ "TaskDownloadMissingLyricsDescription": "Descarga letras de canciones"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index 13e007b4c..210ee4446 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -130,5 +130,7 @@
"TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
"TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen.",
"TaskAudioNormalization": "Normalización de audio",
- "TaskAudioNormalizationDescription": "Escanear archivos para obtener datos de normalización."
+ "TaskAudioNormalizationDescription": "Escanear archivos para obtener datos de normalización.",
+ "TaskDownloadMissingLyricsDescription": "Descargar letras para las canciones",
+ "TaskDownloadMissingLyrics": "Descargar letras faltantes"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json
index e7deefbb0..b458ed423 100644
--- a/Emby.Server.Implementations/Localization/Core/es_419.json
+++ b/Emby.Server.Implementations/Localization/Core/es_419.json
@@ -129,5 +129,7 @@
"TaskAudioNormalization": "Normalización de audio",
"TaskCleanCollectionsAndPlaylistsDescription": "Quita elementos que ya no existen de colecciones y listas de reproducción.",
"TaskAudioNormalizationDescription": "Analiza los archivos para normalizar el audio.",
- "TaskCleanCollectionsAndPlaylists": "Limpieza de colecciones y listas de reproducción"
+ "TaskCleanCollectionsAndPlaylists": "Limpieza de colecciones y listas de reproducción",
+ "TaskDownloadMissingLyrics": "Descargar letra faltante",
+ "TaskDownloadMissingLyricsDescription": "Descarga letras de canciones"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index a13ee48d5..1dba78add 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -15,7 +15,7 @@
"Favorites": "Favoris",
"Folders": "Dossiers",
"Genres": "Genres",
- "HeaderAlbumArtists": "Artistes de l'album",
+ "HeaderAlbumArtists": "Artistes d'albums",
"HeaderContinueWatching": "Continuer de regarder",
"HeaderFavoriteAlbums": "Albums favoris",
"HeaderFavoriteArtists": "Artistes préférés",
@@ -130,5 +130,7 @@
"TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
"TaskCleanCollectionsAndPlaylistsDescription": "Supprime les éléments des collections et des listes de lecture qui n'existent plus.",
"TaskAudioNormalization": "Normalisation audio",
- "TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio."
+ "TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio.",
+ "TaskDownloadMissingLyricsDescription": "Téléchargement des paroles des chansons",
+ "TaskDownloadMissingLyrics": "Télécharger les paroles des chansons manquantes"
}
diff --git a/Emby.Server.Implementations/Localization/Core/km.json b/Emby.Server.Implementations/Localization/Core/km.json
index 02f9d4443..5d10975f3 100644
--- a/Emby.Server.Implementations/Localization/Core/km.json
+++ b/Emby.Server.Implementations/Localization/Core/km.json
@@ -1,3 +1,133 @@
{
- "Albums": "Albums"
+ "Albums": "អាលប៊ុម",
+ "MessageApplicationUpdatedTo": "ម៉ាស៊ីនមេនៃJellyfinត្រូវបានអាប់ដេតទៅកាន់ {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "ការកំណត់ម៉ាស៊ីនមេ ផ្នែក {0} ត្រូវបានអាប់ដេត",
+ "MessageServerConfigurationUpdated": "ការកំណត់ម៉ាស៊ីនមេត្រូវបានអាប់ដេត",
+ "AppDeviceValues": "កម្មវិធី: {0}, ឧបករណ៍: {1}",
+ "MixedContent": "មាតិកាចម្រុះ",
+ "UserLockedOutWithName": "អ្នកប្រើប្រាស់ {0} ត្រូវ​បាន​ផ្អាក",
+ "Application": "កម្មវិធី",
+ "Artists": "សិល្បករ",
+ "AuthenticationSucceededWithUserName": "{0} បានផ្ទៀងផ្ទាត់ដោយជោគជ័យ",
+ "Books": "សៀវភៅ",
+ "NameSeasonNumber": "រដូវកាលទី {0}",
+ "NotificationOptionPluginInstalled": "Plugin បានដំឡើងរួច",
+ "CameraImageUploadedFrom": "រូបភាពកាមេរ៉ាថ្មីត្រូវបានបង្ហោះពី {0}",
+ "Channels": "ប៉ុស្ត៍",
+ "ChapterNameValue": "ជំពូក {0}",
+ "Collections": "បណ្តុំ",
+ "External": "ខាងក្រៅ",
+ "Default": "លំនាំដើម",
+ "NotificationOptionInstallationFailed": "ការដំឡើងមិនបានសម្រេច",
+ "DeviceOfflineWithName": "{0} បានផ្តាច់",
+ "Folders": "ថតឯកសារ",
+ "DeviceOnlineWithName": "{0} បានភ្ចាប់",
+ "HearingImpaired": "ខ្សោយការស្តាប់",
+ "HomeVideos": "វីឌីអូថតខ្លួនឯង",
+ "Favorites": "ចំណូលចិត្ត",
+ "HeaderFavoriteEpisodes": "ភាគដែលចូលចិត្ត",
+ "Forced": "បង្ខំ",
+ "Genres": "ប្រភេទ",
+ "HeaderFavoriteArtists": "សិល្បករដែលចូលចិត្ត",
+ "NotificationOptionApplicationUpdateAvailable": "កម្មវិធី យើងអាចអាប់ដេតបាន",
+ "NotificationOptionApplicationUpdateInstalled": "កម្មវិធី ដែលបានដំឡើងរួច",
+ "NotificationOptionAudioPlaybackStopped": "ការ​ចាក់សម្លេងបានផ្អាក",
+ "HeaderContinueWatching": "បន្តមើល",
+ "HeaderFavoriteAlbums": "អាល់ប៊ុមដែលចូលចិត្ត",
+ "HeaderFavoriteShows": "រឿងភាគដែលចូលចិត្ត",
+ "NewVersionIsAvailable": "មានជំនាន់ថ្មី ម៉ាស៊ីនមេJellyfin អាចទាញយកបាន.",
+ "HeaderAlbumArtists": "សិល្បករអាល់ប៊ុម",
+ "NotificationOptionCameraImageUploaded": "រូបភាពពីកាំមេរ៉ាបានអាប់ឡូតរួច",
+ "HeaderFavoriteSongs": "ចម្រៀងដែលចូលចិត្ត",
+ "HeaderNextUp": "បន្ទាប់",
+ "HeaderLiveTV": "ទូរទស្សន៍ផ្សាយផ្ទាល់",
+ "Movies": "រឿង",
+ "HeaderRecordingGroups": "ក្រុមនៃការថត",
+ "Music": "តន្ត្រី",
+ "Inherit": "មរតក",
+ "MusicVideos": "វីដេអូតន្ត្រី",
+ "NameInstallFailed": "{0} ការដំឡើងបានបរាជ័យ",
+ "NotificationOptionNewLibraryContent": "មាតិកាថ្មីៗត្រូវបានបន្ថែម",
+ "ItemAddedWithName": "{0} ត្រូវបានបន្ថែមទៅបណ្ណាល័យ",
+ "NameSeasonUnknown": "រដូវកាលមិនច្បាស់លាស់",
+ "ItemRemovedWithName": "{0} ត្រូវបានដកចេញពីបណ្ណាល័យ",
+ "LabelIpAddressValue": "លេខ IP: {0}",
+ "LabelRunningTimeValue": "ពេលវេលាកំពុងដំណើរការ: {0}",
+ "Latest": "ចុងក្រោយ",
+ "NotificationOptionAudioPlayback": "ការ​ចាក់​សំឡេង​បាន​ចាប់ផ្ដើម",
+ "NotificationOptionPluginError": "Plugin មិនដំណើរការ",
+ "NotificationOptionPluginUninstalled": "Plugin បានលុបចេញរួច",
+ "MessageApplicationUpdated": "ម៉ាស៊ីនមេនៃJellyfinត្រូវបានអាប់ដេត",
+ "NotificationOptionPluginUpdateInstalled": "Plugin អាប់ដេតបានដំឡើងរួច",
+ "NotificationOptionUserLockedOut": "អ្នកប្រើប្រាស់ត្រូវបានជាប់គាំង",
+ "NotificationOptionServerRestartRequired": "តម្រូវឱ្យចាប់ផ្ដើមម៉ាស៊ីនមេឡើងវិញ",
+ "Photos": "រូបថត",
+ "Playlists": "បញ្ជីចាក់",
+ "Plugin": "Plugin",
+ "PluginInstalledWithName": "{0} ត្រូវបានដំឡើង",
+ "NotificationOptionTaskFailed": "កិច្ចការដែលបានគ្រោងទុកបានបរាជ័យ",
+ "PluginUpdatedWithName": "{0} ត្រូវបានអាប់ដេត",
+ "NotificationOptionVideoPlayback": "ការចាក់វីដេអូបានចាប់ផ្តើម",
+ "Songs": "ចម្រៀង",
+ "ScheduledTaskStartedWithName": "{0} បានចាប់ផ្តើម",
+ "NotificationOptionVideoPlaybackStopped": "ការ​ចាក់​វីដេអូ​បាន​បញ្ឈប់",
+ "PluginUninstalledWithName": "{0} ត្រូវបានលុបចេញ",
+ "Shows": "រឿងភាគ",
+ "ProviderValue": "អ្នកផ្តល់សេវា: {0}",
+ "SubtitleDownloadFailureFromForItem": "សាប់ថាយថលបានបរាជ័យក្នុងការទាញយកពី {0} នៃ {1}",
+ "Sync": "ធ្វើអោយដំណាលគ្នា",
+ "System": "ប្រព័ន្ធ",
+ "TvShows": "កម្មវិធីទូរទស្សន៍",
+ "ScheduledTaskFailedWithName": "{0} បានបរាជ័យ",
+ "Undefined": "មិនបានកំណត់",
+ "User": "អ្នកប្រើប្រាស់",
+ "UserCreatedWithName": "អ្នកប្រើប្រាស់ {0} ត្រូវបានបង្កើតឡើង",
+ "ServerNameNeedsToBeRestarted": "{0} ចាំបាច់ត្រូវចាប់ផ្តើមឡើងវិញ",
+ "StartupEmbyServerIsLoading": "ម៉ាស៊ីនមេJellyfin កំពុងដំណើរការ. សូមព្យាយាមម្តងទៀតក្នុងពេលឆាប់ៗនេះ.",
+ "UserDeletedWithName": "អ្នកប្រើប្រាស់ {0} ត្រូវបានលុបចេញ",
+ "UserOnlineFromDevice": "{0} បានឃើញអនឡានពី {1}",
+ "UserDownloadingItemWithValues": "{0} កំពុងទាញយក {1}",
+ "UserOfflineFromDevice": "{0} បានផ្តាច់ចេញពី {1}",
+ "UserStartedPlayingItemWithValues": "{0} កំពុងចាក់ {1} នៅលើ {2}",
+ "TaskRefreshChapterImagesDescription": "បង្កើតរូបភាពតូចៗសម្រាប់វីដេអូដែលមានតាមជំពូក.",
+ "VersionNumber": "កំណែ {0}",
+ "TasksMaintenanceCategory": "តំហែរទាំ",
+ "TasksLibraryCategory": "បណ្ណាល័យ",
+ "TasksApplicationCategory": "កម្មវិធី",
+ "TaskCleanActivityLog": "សម្អាតកំណត់ហេតុសកម្មភាព",
+ "UserPasswordChangedWithName": "ពាក្យសម្ងាត់ត្រូវបានផ្លាស់ប្តូរសម្រាប់អ្នកប្រើប្រាស់ {0}",
+ "TaskCleanCache": "សម្អាតបញ្ជីឃ្លាំងសម្ងាត់",
+ "TaskRefreshChapterImages": "ដកស្រង់រូបភាពតាមជំពូក",
+ "UserPolicyUpdatedWithName": "គោលការណ៍អ្នកប្រើប្រាស់ត្រូវបានធ្វើបច្ចុប្បន្នភាពសម្រាប់ {0}",
+ "UserStoppedPlayingItemWithValues": "{0} បានបញ្ចប់ការចាក់ {1} នៅលើ {2}",
+ "ValueHasBeenAddedToLibrary": "{0} ត្រូវបានបញ្ចូលទៅក្នុងបណ្ណាល័យរឿងរបស់អ្នក",
+ "ValueSpecialEpisodeName": "ពិសេស - {0}",
+ "TasksChannelsCategory": "ប៉ុស្តតាមអ៊ីនធឺណិត",
+ "TaskAudioNormalization": "ធ្វើឱ្យមានតន្ត្រីមានសម្លេងស្មើគ្នា",
+ "TaskCleanActivityLogDescription": "លុបកំណត់ហេតុសកម្មភាពចាស់ជាងអាយុដែលបានកំណត់រចនាសម្ព័ន្ធ.",
+ "TaskCleanCacheDescription": "លុបឯកសារឃ្លាំងសម្ងាត់លែងត្រូវការដោយប្រព័ន្ធ.",
+ "TaskRefreshLibraryDescription": "ស្កេនបណ្ណាល័យរឿងរបស់អ្នក សម្រាប់ឯកសារថ្មីៗ និងmetadata ឡើងវិញ.",
+ "TaskCleanLogsDescription": "លុបឯកសារកំណត់ហេតុដែលមានអាយុកាលលើសពី {0} ថ្ងៃ.",
+ "TaskRefreshPeopleDescription": "ធ្វើបច្ចុប្បន្នភាព metadata សម្រាប់តួសម្តែង និងអ្នកដឹកនាំនៅក្នុងបណ្ណាល័យរឿងរបស់អ្នក.",
+ "TaskOptimizeDatabaseDescription": "បង្រួម Database និង Truncate free space. ដំណើរការកិច្ចការនេះ បន្ទាប់ពីការស្កេនបណ្ណាល័យ ឬធ្វើការផ្លាស់ប្តូរផ្សេងទៀត ដែលបញ្ជាក់ថា ការកែប្រែ Database អាចធ្វើឱ្យដំណើរការប្រសើរឡើង.",
+ "TaskRefreshTrickplayImages": "បង្កើតបណ្តុំរូបភាពតាម Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "បង្កើត​ trickplay previews សម្រាប់​វីដេអូ​ក្នុង​បណ្ណាល័យ​ដែល​បានបង្ហាញ.",
+ "TaskKeyframeExtractorDescription": "ស្រង់យកFrame គន្លឹះៗពីវីដេអូ ដើម្បីបង្កើតបញ្ជីចាក់ HLS ច្បាស់លាស់ជាងមុន. កិច្ចការនេះអាចនឹងដំណើរការយូរ.",
+ "FailedLoginAttemptWithUserName": "បរាជ័យក្នុងការព្យាយាមចូលពី {0}",
+ "TaskCleanTranscode": "សម្អាតថតឯកសារ ​Transcode",
+ "TaskRefreshChannelsDescription": "Refreshes ព័ត៌មានបណ្តាញអ៊ីនធឺណិត.",
+ "TaskDownloadMissingSubtitles": "ទាញយកសាប់ថាយថលដែលបាត់",
+ "TaskRefreshChannels": "Refresh ឆានែល",
+ "TaskKeyframeExtractor": "ការញែក Keyframe",
+ "TaskAudioNormalizationDescription": "ស្កែនឯកសារសម្រាប់ធ្វើឱ្យមានតន្ត្រីមានសម្លេងស្មើគ្នា.",
+ "TaskRefreshLibrary": "ស្កេនបណ្ណាល័យរឿង",
+ "TaskCleanLogs": "សម្អាត Log Directory",
+ "TaskRefreshPeople": "Refresh អ្នកប្រើប្រាស់",
+ "TaskUpdatePlugins": "ធ្វើបច្ចុប្បន្នភាព Plugins",
+ "TaskUpdatePluginsDescription": "ទាញយក និងដំឡើងបច្ចុប្បន្នភាពសម្រាប់Plugins ដែលត្រូវបាន Config ដើម្បីធ្វើបច្ចុប្បន្នភាពដោយស្វ័យប្រវត្តិ.",
+ "TaskCleanTranscodeDescription": "លុបឯកសារ Transcode ដែលលើសពីមួយថ្ងៃ.",
+ "TaskDownloadMissingSubtitlesDescription": "ស្វែងរកតាមអ៊ីនធឺណិត សម្រាប់សាប់ថាយថល ដែលបាត់ដោយផ្អែកលើ metadata.",
+ "TaskOptimizeDatabase": "ធ្វើឱ្យ Database ប្រសើរឡើង",
+ "TaskCleanCollectionsAndPlaylistsDescription": "លុបរបស់របរចេញពីបណ្តុំ និងបញ្ជីចាក់ដែលលែងមាន.",
+ "TaskCleanCollectionsAndPlaylists": "សម្អាតបណ្តុំ និងបញ្ជីចាក់"
}
diff --git a/Emby.Server.Implementations/Localization/Core/kw.json b/Emby.Server.Implementations/Localization/Core/kw.json
new file mode 100644
index 000000000..ffb4345c8
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/kw.json
@@ -0,0 +1,135 @@
+{
+ "Collections": "Kuntellow",
+ "DeviceOfflineWithName": "{0} re anjunyas",
+ "External": "A-ves",
+ "Folders": "Plegellow",
+ "HeaderFavoriteAlbums": "Albomow Drudh",
+ "HeaderFavoriteArtists": "Artydhyon Drudh",
+ "HeaderFavoriteEpisodes": "Towlennow Drudh",
+ "HeaderFavoriteSongs": "Kanow Drudh",
+ "HeaderRecordingGroups": "Bagasow Rekordya",
+ "HearingImpaired": "Klewans Aperys",
+ "HomeVideos": "Gwydhyow Tre",
+ "Inherit": "Herya",
+ "LabelRunningTimeValue": "Prys ow ponya: {0}",
+ "Latest": "Diwettha",
+ "MessageApplicationUpdated": "Servell Jellyfin re beu nowedhys",
+ "MessageApplicationUpdatedTo": "Servell Jellyfin re beu nowedhys dhe {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Rann dewisyans servell {0} re beu nowedhys",
+ "MixedContent": "Dalgh kemmyskys",
+ "Movies": "Fylmow",
+ "MusicVideos": "Gwydhyow Ilow",
+ "NameSeasonUnknown": "Seson Anwodhvedhys",
+ "NotificationOptionAudioPlayback": "Seneans dallethys",
+ "NotificationOptionAudioPlaybackStopped": "Seneans hedhys",
+ "NotificationOptionPluginError": "Defowt ystynnans",
+ "NotificationOptionPluginUninstalled": "Ystynnans anynstallys",
+ "NotificationOptionPluginUpdateInstalled": "Nowedheans ystynnans ynstallys",
+ "Application": "Gweythres",
+ "Favorites": "Moyha Kerys",
+ "Forced": "Konstrynys",
+ "Albums": "Albomow",
+ "Books": "Lyvrow",
+ "Channels": "Kanolyow",
+ "AppDeviceValues": "App: {0}, Devis: {1}",
+ "Artists": "Artyhdyon",
+ "HeaderAlbumArtists": "Albom artydhyon",
+ "HeaderNextUp": "Nessa",
+ "CameraImageUploadedFrom": "Skeusen kamera nowydh re beu ughkargys a-dhyworth {0}",
+ "ChapterNameValue": "Chaptra {0}",
+ "FailedLoginAttemptWithUserName": "Assay omgelm fyllys a-dhyworth {0}",
+ "AuthenticationSucceededWithUserName": "{0} omgelmys yn sewen",
+ "Default": "Defowt",
+ "DeviceOnlineWithName": "{0} yw junys",
+ "ItemRemovedWithName": "{0} a veu dileys a-dhyworth an lyverva",
+ "LabelIpAddressValue": "Trigva PK: {)}",
+ "Music": "Ilow",
+ "HeaderContinueWatching": "Pesya Ow Kweles",
+ "NameSeasonNumber": "Seson {0}",
+ "NotificationOptionApplicationUpdateInstalled": "Nowedheans gweythres ynstallys",
+ "NotificationOptionCameraImageUploaded": "Skeusen kamera ughkargys",
+ "HeaderFavoriteShows": "Diskwedhyansow Drudh",
+ "HeaderLiveTV": "PW Yn Fyw",
+ "MessageServerConfigurationUpdated": "Dewisyans servell re beu nowedhys",
+ "ItemAddedWithName": "{0} a veu keworrys dhe'n lyverva",
+ "NameInstallFailed": "{0} ynstallyans fyllys",
+ "NotificationOptionNewLibraryContent": "Dalgh nowydh keworrys",
+ "NewVersionIsAvailable": "Yma versyon nowydh a Servell Jellyfin neb yw kavadow rag iskarga.",
+ "NotificationOptionApplicationUpdateAvailable": "Nowedheans gweythres kavadow",
+ "NotificationOptionInstallationFailed": "Defowt ynstallyans",
+ "Genres": "Eghennow",
+ "NotificationOptionPluginInstalled": "Ystynnans ynstallys",
+ "NotificationOptionServerRestartRequired": "Dastalleth servell yw res",
+ "StartupEmbyServerIsLoading": "Yma Servell Jellyfin ow kargya. Assay arta yn berr mar pleg.",
+ "SubtitleDownloadFailureFromForItem": "Istitlow a fyllis iskarga a-dhyworth {0] rag {1}",
+ "System": "Kevreyth",
+ "User": "Devnydhyer",
+ "UserDeletedWithName": "Devnydhyer {0} re beu dileys",
+ "UserLockedOutWithName": "Devnydhyer {0} re beu alhwedhys yn-mes",
+ "UserStoppedPlayingItemWithValues": "{0} re worfennas gwari {1} war {2}",
+ "UserOfflineFromDevice": "{0} re anjunyas a-dhyworth {1}",
+ "UserOnlineFromDevice": "{0} yw warlinen a-dhyworth {1}",
+ "NotificationOptionUserLockedOut": "Devnydhyer yw alhwedhys yn-mes",
+ "Photos": "Skeusennow",
+ "Playlists": "Rolyow-gwari",
+ "Plugin": "Ystynnans",
+ "PluginInstalledWithName": "{0} a veu ynstallys",
+ "UserPolicyUpdatedWithName": "Polici devnydhyer re beu nowedhys rag {0}",
+ "PluginUpdatedWithName": "{0} a veu nowedhys",
+ "ScheduledTaskFailedWithName": "{0} a fyllis",
+ "Songs": "Kanow",
+ "Sync": "Kesseni",
+ "TvShows": "Towlennow PW",
+ "Undefined": "Anstyrys",
+ "UserCreatedWithName": "Devnydhyer {0} re beu gwruthys",
+ "UserDownloadingItemWithValues": "Yma {0} owth iskarga {1}",
+ "UserPasswordChangedWithName": "Ger-tremena re beu chanjys rag devnydhyer {0}",
+ "UserStartedPlayingItemWithValues": "Yma {0} ow kwari {1} war {2}",
+ "ValueHasBeenAddedToLibrary": "{0} re beu keworrys dhe'th lyverva media",
+ "VersionNumber": "Versyon {0}",
+ "TasksLibraryCategory": "Lyverva",
+ "TaskCleanActivityLog": "Glanhe Kovlyver Gwrians",
+ "TaskRefreshPeople": "Disegha Tus",
+ "TaskRefreshLibrary": "Arhwilas Lyverva Media",
+ "TaskCleanTranscodeDescription": "Y hwra dilea restrennow treylya neg a veu gwrys kyns nans yw dydh.",
+ "NotificationOptionVideoPlaybackStopped": "Gwareans gwydhyow yw hedhys",
+ "NotificationOptionVideoPlayback": "Gwareans gwydhyow yw dallethys",
+ "PluginUninstalledWithName": "{0} a veu anynstallys",
+ "NotificationOptionTaskFailed": "Defowt oberen towlennys",
+ "ProviderValue": "Provier: {0}",
+ "ScheduledTaskStartedWithName": "{0} a dhallathas",
+ "ServerNameNeedsToBeRestarted": "Yma edhom dhe {0} a vos dastallathys",
+ "ValueSpecialEpisodeName": "Arbennik - {0}",
+ "TasksMaintenanceCategory": "Mentons",
+ "TasksApplicationCategory": "Gweythres",
+ "TasksChannelsCategory": "Kanolyow Kesrosweyth",
+ "TaskCleanLogs": "Glanhe Kevarwodhyador Kovlyver",
+ "TaskAudioNormalization": "Normalheans Klewans",
+ "TaskRefreshChannels": "Disegha Kanolyow",
+ "TaskCleanTranscode": "Glanhe Kevarwodhyador Treylya",
+ "TaskUpdatePlugins": "Nowedhi Ystynansow",
+ "Shows": "Diskwedhyansow",
+ "TaskCleanCache": "Glanhe Kevarwodhyador Gwithva",
+ "TaskCleanActivityLogDescription": "Y hwra dilea lin kovlyver gwrians kottha ages an bloodh dewisys.",
+ "TaskCleanCacheDescription": "Y hwra dilea restrennow gwithva nag yw res rag an kevreyth.",
+ "TaskRefreshPeopleDescription": "Y hwra nowedhi metadata rag gwarioryon ha kevarwodhoryon yn dha lyverva media.",
+ "TaskRefreshChapterImages": "Kuntel Imajys Chaptra",
+ "TaskRefreshChapterImagesDescription": "Y hwra ewines meus rag gwydhyowyow gans chaptraow.",
+ "TaskRefreshTrickplayImagesDescription": "Y hwra kynwelyow trickplay rag gwydhyowyow yn lyvervaow gallosegys.",
+ "TaskRefreshTrickplayImages": "Dinythi Imajys Trickplay",
+ "TaskCleanLogsDescription": "Y hwra dilea restrennow kovlyver a veu gwrys kyns nans yw {0} dydh.",
+ "TaskDownloadMissingLyrics": "Iskarga geryow kellys",
+ "TaskUpdatePluginsDescription": "Y hwra iskarga hag ynstallya nowedheansow rag ystynansow neb yw dewisys dhe nowedhi yn awtomatek.",
+ "TaskDownloadMissingSubtitles": "Iskarga istitlow kellys",
+ "TaskRefreshChannelsDescription": "Y hwra disegha kedhlow kanolyow kesrosweyth.",
+ "TaskDownloadMissingLyricsDescription": "Y hwra iskarga geryow rag kanow",
+ "TaskDownloadMissingSubtitlesDescription": "Y hwra hwilas an kesrosweyth rag istitlow kellys a-dhywoth dewisyans metadata.",
+ "TaskOptimizeDatabase": "Gwellhe selvanylyon",
+ "TaskOptimizeDatabaseDescription": "Y hwra kesstrotha ha berrhe efander rydh. Martesen y hwra gwellhe gwryth mar kwre'ta an oberen ma wosa ty dhe arhwilas an lyverva, po neb chanj aral neb a brof chanjyansow selvanylyon.",
+ "TaskAudioNormalizationDescription": "Y hwra arhwilas restrennow rag manylyon normalheans klewans.",
+ "TaskRefreshLibraryDescription": "Y hwra arhwilas dha lyverva media rag restrennow nowydh ha disegha metamanylyon.",
+ "TaskCleanCollectionsAndPlaylists": "Glanhe kuntellow ha rolyow-gwari",
+ "TaskKeyframeExtractor": "Estennell Framalhwedh",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Y hwra dilea taklow a-dhyworth kuntellow ha rolyow-gwari na vos na moy.",
+ "TaskKeyframeExtractorDescription": "Y hwra kuntel framyowalhwedh a-dhyworth restrennow gwydhyowyow rag gul rolyow-gwari HLS moy poran. Martesen y hwra an oberen ma ow ponya rag termyn hir."
+}
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index b66818ddc..747652538 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -130,5 +130,7 @@
"TaskCleanCollectionsAndPlaylists": "Rydd kolleksjoner og spillelister",
"TaskAudioNormalization": "Lyd Normalisering",
"TaskAudioNormalizationDescription": "Skan filer for lyd normaliserende data",
- "TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra kolleksjoner og spillelister som ikke lengere finnes"
+ "TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra kolleksjoner og spillelister som ikke lengere finnes",
+ "TaskDownloadMissingLyrics": "Last ned manglende tekster",
+ "TaskDownloadMissingLyricsDescription": "Last ned sangtekster"
}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index 4f076b680..39e7cd546 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -25,7 +25,7 @@
"HeaderLiveTV": "Live TV",
"HeaderNextUp": "Volgende",
"HeaderRecordingGroups": "Opnamegroepen",
- "HomeVideos": "Thuis video's",
+ "HomeVideos": "Homevideo's",
"Inherit": "Erven",
"ItemAddedWithName": "{0} is toegevoegd aan de bibliotheek",
"ItemRemovedWithName": "{0} is verwijderd uit de bibliotheek",
@@ -130,5 +130,7 @@
"TaskCleanCollectionsAndPlaylists": "Collecties en afspeellijsten opruimen",
"TaskCleanCollectionsAndPlaylistsDescription": "Verwijdert niet langer bestaande items uit collecties en afspeellijsten.",
"TaskAudioNormalization": "Geluidsnormalisatie",
- "TaskAudioNormalizationDescription": "Scant bestanden op gegevens voor geluidsnormalisatie."
+ "TaskAudioNormalizationDescription": "Scant bestanden op gegevens voor geluidsnormalisatie.",
+ "TaskDownloadMissingLyrics": "Ontbrekende liedteksten downloaden",
+ "TaskDownloadMissingLyricsDescription": "Downloadt liedteksten"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json
index f36385be2..a24a837ab 100644
--- a/Emby.Server.Implementations/Localization/Core/pl.json
+++ b/Emby.Server.Implementations/Localization/Core/pl.json
@@ -130,5 +130,7 @@
"TaskCleanCollectionsAndPlaylistsDescription": "Usuwa elementy z kolekcji i list odtwarzania, które już nie istnieją.",
"TaskCleanCollectionsAndPlaylists": "Oczyść kolekcje i listy odtwarzania",
"TaskAudioNormalization": "Normalizacja dźwięku",
- "TaskAudioNormalizationDescription": "Skanuje pliki w poszukiwaniu danych normalizacji dźwięku."
+ "TaskAudioNormalizationDescription": "Skanuje pliki w poszukiwaniu danych normalizacji dźwięku.",
+ "TaskDownloadMissingLyrics": "Pobierz brakujące słowa",
+ "TaskDownloadMissingLyricsDescription": "Pobierz słowa piosenek"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json
index cd0120fc7..2f52aafa3 100644
--- a/Emby.Server.Implementations/Localization/Core/ro.json
+++ b/Emby.Server.Implementations/Localization/Core/ro.json
@@ -125,5 +125,9 @@
"TaskKeyframeExtractor": "Extractor de cadre cheie",
"HearingImpaired": "Ascultare Impară",
"TaskRefreshTrickplayImages": "Generează imagini Trickplay",
- "TaskRefreshTrickplayImagesDescription": "Generează previzualizările trickplay pentru videourile din librăriile selectate."
+ "TaskRefreshTrickplayImagesDescription": "Generează previzualizările trickplay pentru videourile din librăriile selectate.",
+ "TaskAudioNormalizationDescription": "Scanează fișiere pentru date necesare normalizării sunetului.",
+ "TaskAudioNormalization": "Normalizare sunet",
+ "TaskCleanCollectionsAndPlaylists": "Curăță colecțiile și listele de redare",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Elimină elementele care nu mai există din colecții și liste de redare."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index 3eb1e0468..01b8bfbe2 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -130,5 +130,7 @@
"TaskCleanCollectionsAndPlaylists": "Очистка коллекций и списков воспроизведения",
"TaskCleanCollectionsAndPlaylistsDescription": "Удаляет элементы из коллекций и списков воспроизведения, которые больше не существуют.",
"TaskAudioNormalization": "Нормализация звука",
- "TaskAudioNormalizationDescription": "Сканирует файлы на наличие данных о нормализации звука."
+ "TaskAudioNormalizationDescription": "Сканирует файлы на наличие данных о нормализации звука.",
+ "TaskDownloadMissingLyrics": "Загрузить недостающий текст",
+ "TaskDownloadMissingLyricsDescription": "Загружает текст песен"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index f40c4478a..a4e2302d1 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/Emby.Server.Implementations/Localization/Core/sv.json
@@ -16,7 +16,7 @@
"Folders": "Mappar",
"Genres": "Genrer",
"HeaderAlbumArtists": "Albumartister",
- "HeaderContinueWatching": "Fortsätt titta på",
+ "HeaderContinueWatching": "Fortsätt titta",
"HeaderFavoriteAlbums": "Favoritalbum",
"HeaderFavoriteArtists": "Favoritartister",
"HeaderFavoriteEpisodes": "Favoritavsnitt",
@@ -130,5 +130,7 @@
"TaskCleanCollectionsAndPlaylists": "Rensa upp samlingar och spellistor",
"TaskAudioNormalization": "Ljudnormalisering",
"TaskCleanCollectionsAndPlaylistsDescription": "Tar bort objekt från samlingar och spellistor som inte längre finns.",
- "TaskAudioNormalizationDescription": "Skannar filer för ljudnormaliseringsdata."
+ "TaskAudioNormalizationDescription": "Skannar filer för ljudnormaliseringsdata.",
+ "TaskDownloadMissingLyrics": "Ladda ner saknad låttext",
+ "TaskDownloadMissingLyricsDescription": "Laddar ner låttexter"
}
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
index 18073287b..97bad4532 100644
--- a/Emby.Server.Implementations/Localization/Core/uk.json
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -129,5 +129,7 @@
"TaskCleanCollectionsAndPlaylists": "Очистити колекції і списки відтворення",
"TaskCleanCollectionsAndPlaylistsDescription": "Видаляє елементи з колекцій і списків відтворення, які більше не існують.",
"TaskAudioNormalizationDescription": "Сканує файли на наявність даних для нормалізації звуку.",
- "TaskAudioNormalization": "Нормалізація аудіо"
+ "TaskAudioNormalization": "Нормалізація аудіо",
+ "TaskDownloadMissingLyrics": "Завантажити відсутні тексти пісень",
+ "TaskDownloadMissingLyricsDescription": "Завантаження текстів пісень"
}
diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json
index 4bedfe3b2..32e2f4bab 100644
--- a/Emby.Server.Implementations/Localization/Core/vi.json
+++ b/Emby.Server.Implementations/Localization/Core/vi.json
@@ -129,5 +129,7 @@
"TaskCleanCollectionsAndPlaylists": "Dọn dẹp bộ sưu tập và danh sách phát",
"TaskCleanCollectionsAndPlaylistsDescription": "Xóa các mục khỏi bộ sưu tập và danh sách phát không còn tồn tại.",
"TaskAudioNormalization": "Chuẩn Hóa Âm Thanh",
- "TaskAudioNormalizationDescription": "Quét tập tin để tìm dữ liệu chuẩn hóa âm thanh."
+ "TaskAudioNormalizationDescription": "Quét tập tin để tìm dữ liệu chuẩn hóa âm thanh.",
+ "TaskDownloadMissingLyricsDescription": "Tải xuống lời cho bài hát",
+ "TaskDownloadMissingLyrics": "Tải xuống lời bị thiếu"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index 808f73793..4bec590fb 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json
@@ -130,5 +130,7 @@
"TaskCleanCollectionsAndPlaylists": "清理合集和播放列表",
"TaskCleanCollectionsAndPlaylistsDescription": "清理合集和播放列表中已不存在的项目。",
"TaskAudioNormalization": "音频标准化",
- "TaskAudioNormalizationDescription": "扫描文件以寻找音频标准化数据。"
+ "TaskAudioNormalizationDescription": "扫描文件以寻找音频标准化数据。",
+ "TaskDownloadMissingLyrics": "下载缺失的歌词",
+ "TaskDownloadMissingLyricsDescription": "下载歌曲歌词"
}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 3dda5fdee..681c252b6 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -237,7 +237,7 @@ namespace Emby.Server.Implementations.Session
ArgumentException.ThrowIfNullOrEmpty(deviceId);
var activityDate = DateTime.UtcNow;
- var session = await GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
+ var session = GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
var lastActivityDate = session.LastActivityDate;
session.LastActivityDate = activityDate;
@@ -435,7 +435,7 @@ namespace Emby.Server.Implementations.Session
/// <param name="remoteEndPoint">The remote end point.</param>
/// <param name="user">The user.</param>
/// <returns>SessionInfo.</returns>
- private async Task<SessionInfo> GetSessionInfo(
+ private SessionInfo GetSessionInfo(
string appName,
string appVersion,
string deviceId,
@@ -453,7 +453,7 @@ namespace Emby.Server.Implementations.Session
if (!_activeConnections.TryGetValue(key, out var sessionInfo))
{
- sessionInfo = await CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
+ sessionInfo = CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
_activeConnections[key] = sessionInfo;
}
@@ -478,7 +478,7 @@ namespace Emby.Server.Implementations.Session
return sessionInfo;
}
- private async Task<SessionInfo> CreateSession(
+ private SessionInfo CreateSession(
string key,
string appName,
string appVersion,
@@ -508,7 +508,7 @@ namespace Emby.Server.Implementations.Session
deviceName = "Network Device";
}
- var deviceOptions = await _deviceManager.GetDeviceOptions(deviceId).ConfigureAwait(false);
+ var deviceOptions = _deviceManager.GetDeviceOptions(deviceId);
if (string.IsNullOrEmpty(deviceOptions.CustomName))
{
sessionInfo.DeviceName = deviceName;
@@ -1297,7 +1297,7 @@ namespace Emby.Server.Implementations.Session
return new[] { item };
}
- private IEnumerable<BaseItem> TranslateItemForInstantMix(Guid id, User user)
+ private List<BaseItem> TranslateItemForInstantMix(Guid id, User user)
{
var item = _libraryManager.GetItemById(id);
@@ -1307,7 +1307,7 @@ namespace Emby.Server.Implementations.Session
return new List<BaseItem>();
}
- return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false });
+ return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false }).ToList();
}
/// <inheritdoc />
@@ -1520,12 +1520,12 @@ namespace Emby.Server.Implementations.Session
// This should be validated above, but if it isn't don't delete all tokens.
ArgumentException.ThrowIfNullOrEmpty(deviceId);
- var existing = (await _deviceManager.GetDevices(
+ var existing = _deviceManager.GetDevices(
new DeviceQuery
{
DeviceId = deviceId,
UserId = user.Id
- }).ConfigureAwait(false)).Items;
+ }).Items;
foreach (var auth in existing)
{
@@ -1553,12 +1553,12 @@ namespace Emby.Server.Implementations.Session
ArgumentException.ThrowIfNullOrEmpty(accessToken);
- var existing = (await _deviceManager.GetDevices(
+ var existing = _deviceManager.GetDevices(
new DeviceQuery
{
Limit = 1,
AccessToken = accessToken
- }).ConfigureAwait(false)).Items;
+ }).Items;
if (existing.Count > 0)
{
@@ -1597,10 +1597,10 @@ namespace Emby.Server.Implementations.Session
{
CheckDisposed();
- var existing = await _deviceManager.GetDevices(new DeviceQuery
+ var existing = _deviceManager.GetDevices(new DeviceQuery
{
UserId = userId
- }).ConfigureAwait(false);
+ });
foreach (var info in existing.Items)
{
@@ -1787,11 +1787,11 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc />
public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
{
- var items = (await _deviceManager.GetDevices(new DeviceQuery
+ var items = _deviceManager.GetDevices(new DeviceQuery
{
AccessToken = token,
Limit = 1
- }).ConfigureAwait(false)).Items;
+ }).Items;
if (items.Count == 0)
{
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index c1a615666..d11b03a2e 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -19,14 +19,12 @@ namespace Emby.Server.Implementations.TV
{
public class TVSeriesManager : ITVSeriesManager
{
- private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataManager;
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _configurationManager;
- public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager)
+ public TVSeriesManager(IUserDataManager userDataManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager)
{
- _userManager = userManager;
_userDataManager = userDataManager;
_libraryManager = libraryManager;
_configurationManager = configurationManager;
@@ -34,12 +32,7 @@ namespace Emby.Server.Implementations.TV
public QueryResult<BaseItem> GetNextUp(NextUpQuery query, DtoOptions options)
{
- var user = _userManager.GetUserById(query.UserId);
-
- if (user is null)
- {
- throw new ArgumentException("User not found");
- }
+ var user = query.User;
string? presentationUniqueKey = null;
if (!query.SeriesId.IsNullOrEmpty())
@@ -83,15 +76,10 @@ namespace Emby.Server.Implementations.TV
public QueryResult<BaseItem> GetNextUp(NextUpQuery request, BaseItem[] parentsFolders, DtoOptions options)
{
- var user = _userManager.GetUserById(request.UserId);
-
- if (user is null)
- {
- throw new ArgumentException("User not found");
- }
+ var user = request.User;
string? presentationUniqueKey = null;
- int? limit = request.Limit;
+ int? limit = null;
if (!request.SeriesId.IsNullOrEmpty())
{
if (_libraryManager.GetItemById(request.SeriesId.Value) is Series series)
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 8954c8ef5..a47c60473 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -46,7 +46,7 @@ public class AudioController : BaseJellyfinApiController
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="audioCodec">Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
@@ -76,7 +76,7 @@ public class AudioController : BaseJellyfinApiController
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -213,7 +213,7 @@ public class AudioController : BaseJellyfinApiController
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="audioCodec">Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
@@ -243,7 +243,7 @@ public class AudioController : BaseJellyfinApiController
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index 6d9ec343e..2a2ab4ad1 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -47,10 +47,10 @@ public class DevicesController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] Guid? userId)
+ public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
- return await _deviceManager.GetDevicesForUser(userId).ConfigureAwait(false);
+ return _deviceManager.GetDevicesForUser(userId);
}
/// <summary>
@@ -63,9 +63,9 @@ public class DevicesController : BaseJellyfinApiController
[HttpGet("Info")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<DeviceInfo>> GetDeviceInfo([FromQuery, Required] string id)
+ public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string id)
{
- var deviceInfo = await _deviceManager.GetDevice(id).ConfigureAwait(false);
+ var deviceInfo = _deviceManager.GetDevice(id);
if (deviceInfo is null)
{
return NotFound();
@@ -84,9 +84,9 @@ public class DevicesController : BaseJellyfinApiController
[HttpGet("Options")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<DeviceOptions>> GetDeviceOptions([FromQuery, Required] string id)
+ public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string id)
{
- var deviceInfo = await _deviceManager.GetDeviceOptions(id).ConfigureAwait(false);
+ var deviceInfo = _deviceManager.GetDeviceOptions(id);
if (deviceInfo is null)
{
return NotFound();
@@ -124,13 +124,13 @@ public class DevicesController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DeleteDevice([FromQuery, Required] string id)
{
- var existingDevice = await _deviceManager.GetDevice(id).ConfigureAwait(false);
+ var existingDevice = _deviceManager.GetDevice(id);
if (existingDevice is null)
{
return NotFound();
}
- var sessions = await _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }).ConfigureAwait(false);
+ var sessions = _deviceManager.GetDevices(new DeviceQuery { DeviceId = id });
foreach (var session in sessions.Items)
{
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 130c1192f..662e2acbc 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -45,6 +45,7 @@ public class DynamicHlsController : BaseJellyfinApiController
private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0);
+ private readonly Version _minFFmpegX265BframeInFmp4 = new Version(7, 0, 1);
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
@@ -116,7 +117,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="audioCodec">Optional. Specify an audio codec to encode to, e.g. mp3.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
@@ -146,7 +147,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -355,7 +356,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="audioCodec">Optional. Specify an audio codec to encode to, e.g. mp3.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
@@ -387,7 +388,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -531,7 +532,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="audioCodec">Optional. Specify an audio codec to encode to, e.g. mp3.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
@@ -562,7 +563,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -700,7 +701,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="audioCodec">Optional. Specify an audio codec to encode to, e.g. mp3.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
@@ -732,7 +733,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -871,7 +872,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="audioCodec">Optional. Specify an audio codec to encode to, e.g. mp3.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
@@ -902,7 +903,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -1043,7 +1044,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="audioCodec">Optional. Specify an audio codec to encode to, e.g. mp3.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
@@ -1075,7 +1076,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -1227,7 +1228,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="audioCodec">Optional. Specify an audio codec to encode to, e.g. mp3.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
@@ -1258,7 +1259,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -1851,13 +1852,12 @@ public class DynamicHlsController : BaseJellyfinApiController
args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, isEventPlaylist, startNumber);
// Currently b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
- if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase)
+ && _mediaEncoder.EncoderVersion < _minFFmpegX265BframeInFmp4)
{
args += " -bf 0";
}
- // args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
-
// video processing filters.
var videoProcessParam = _encodingHelper.GetVideoProcessingFilterParam(state, _encodingOptions, codec);
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 8e8accab3..b71199026 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -109,7 +109,7 @@ public class ImageController : BaseJellyfinApiController
return NotFound();
}
- if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, requestUserId, true))
+ if (!RequestHelpers.AssertCanUpdateUser(HttpContext.User, user, true))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
}
@@ -203,13 +203,18 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] Guid? userId)
{
var requestUserId = RequestHelpers.GetUserId(User, userId);
- if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, requestUserId, true))
+ var user = _userManager.GetUserById(requestUserId);
+ if (user is null)
+ {
+ return NotFound();
+ }
+
+ if (!RequestHelpers.AssertCanUpdateUser(HttpContext.User, user, true))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
}
- var user = _userManager.GetUserById(requestUserId);
- if (user?.ProfileImage is null)
+ if (user.ProfileImage is null)
{
return NoContent();
}
@@ -2089,6 +2094,8 @@ public class ImageController : BaseJellyfinApiController
Response.Headers.Append(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
Response.Headers.Append(HeaderNames.Vary, HeaderNames.Accept);
+ Response.Headers.ContentDisposition = "attachment";
+
if (disableCaching)
{
Response.Headers.Append(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index d33634412..828bd5174 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -972,12 +972,17 @@ public class ItemsController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId)
{
var requestUserId = RequestHelpers.GetUserId(User, userId);
- if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, requestUserId, true))
+ var user = _userManager.GetUserById(requestUserId);
+ if (user is null)
+ {
+ return NotFound();
+ }
+
+ if (!RequestHelpers.AssertCanUpdateUser(User, user, true))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to view this item user data.");
}
- var user = _userManager.GetUserById(requestUserId) ?? throw new ResourceNotFoundException();
var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
@@ -1023,12 +1028,17 @@ public class ItemsController : BaseJellyfinApiController
[FromBody, Required] UpdateUserItemDataDto userDataDto)
{
var requestUserId = RequestHelpers.GetUserId(User, userId);
- if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, requestUserId, true))
+ var user = _userManager.GetUserById(requestUserId);
+ if (user is null)
+ {
+ return NotFound();
+ }
+
+ if (!RequestHelpers.AssertCanUpdateUser(User, user, true))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update this item user data.");
}
- var user = _userManager.GetUserById(requestUserId) ?? throw new ResourceNotFoundException();
var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 62cb59335..afc93c3a8 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -857,6 +857,16 @@ public class LibraryController : BaseJellyfinApiController
.DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.ToArray();
+ result.LyricFetchers = plugins
+ .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.LyricFetcher))
+ .Select(i => new LibraryOptionInfoDto
+ {
+ Name = i.Name,
+ DefaultEnabled = true
+ })
+ .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+
var typeOptions = new List<LibraryTypeOptionsDto>();
foreach (var type in types)
diff --git a/Jellyfin.Api/Controllers/MediaSegmentsController.cs b/Jellyfin.Api/Controllers/MediaSegmentsController.cs
new file mode 100644
index 000000000..e97704d48
--- /dev/null
+++ b/Jellyfin.Api/Controllers/MediaSegmentsController.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Threading.Tasks;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.MediaSegments;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Media Segments api.
+/// </summary>
+[Authorize]
+public class MediaSegmentsController : BaseJellyfinApiController
+{
+ private readonly IMediaSegmentManager _mediaSegmentManager;
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MediaSegmentsController"/> class.
+ /// </summary>
+ /// <param name="mediaSegmentManager">MediaSegments Manager.</param>
+ /// <param name="libraryManager">The Library manager.</param>
+ public MediaSegmentsController(IMediaSegmentManager mediaSegmentManager, ILibraryManager libraryManager)
+ {
+ _mediaSegmentManager = mediaSegmentManager;
+ _libraryManager = libraryManager;
+ }
+
+ /// <summary>
+ /// Gets all media segments based on an itemId.
+ /// </summary>
+ /// <param name="itemId">The ItemId.</param>
+ /// <param name="includeSegmentTypes">Optional filter of requested segment types.</param>
+ /// <returns>A list of media segment objects related to the requested itemId.</returns>
+ [HttpGet("{itemId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<QueryResult<MediaSegmentDto>>> GetSegmentsAsync(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] IEnumerable<MediaSegmentType>? includeSegmentTypes = null)
+ {
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
+ if (item is null)
+ {
+ return NotFound();
+ }
+
+ var items = await _mediaSegmentManager.GetSegmentsAsync(item.Id, includeSegmentTypes).ConfigureAwait(false);
+ return Ok(new QueryResult<MediaSegmentDto>(items.ToArray()));
+ }
+}
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index 6abd7a23e..53b7349e7 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -233,6 +233,8 @@ public class PluginsController : BaseJellyfinApiController
return NotFound();
}
+ Response.Headers.ContentDisposition = "attachment";
+
imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath);
return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath));
}
diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs
index 0afe053da..60d49af9e 100644
--- a/Jellyfin.Api/Controllers/TrickplayController.cs
+++ b/Jellyfin.Api/Controllers/TrickplayController.cs
@@ -95,6 +95,7 @@ public class TrickplayController : BaseJellyfinApiController
var path = _trickplayManager.GetTrickplayTilePath(item, width, index);
if (System.IO.File.Exists(path))
{
+ Response.Headers.ContentDisposition = "attachment";
return PhysicalFile(path, MediaTypeNames.Image.Jpeg);
}
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 426402667..914ccd7f9 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -90,7 +90,12 @@ public class TvShowsController : BaseJellyfinApiController
[FromQuery] bool enableResumable = true,
[FromQuery] bool enableRewatching = false)
{
- userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(RequestHelpers.GetUserId(User, userId));
+ if (user is null)
+ {
+ return NotFound();
+ }
+
var options = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@@ -102,7 +107,7 @@ public class TvShowsController : BaseJellyfinApiController
ParentId = parentId,
SeriesId = seriesId,
StartIndex = startIndex,
- UserId = userId.Value,
+ User = user,
EnableTotalRecordCount = enableTotalRecordCount,
DisableFirstEpisode = disableFirstEpisode,
NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue,
@@ -111,10 +116,6 @@ public class TvShowsController : BaseJellyfinApiController
},
options);
- var user = userId.IsNullOrEmpty()
- ? null
- : _userManager.GetUserById(userId.Value);
-
var returnItems = _dtoService.GetBaseItemDtos(result.Items, options, user);
return new QueryResult<BaseItemDto>(
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index c3923a2ad..2df79c80c 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -274,16 +274,15 @@ public class UserController : BaseJellyfinApiController
[FromBody, Required] UpdateUserPassword request)
{
var requestUserId = userId ?? User.GetUserId();
- if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, requestUserId, true))
+ var user = _userManager.GetUserById(requestUserId);
+ if (user is null)
{
- return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the password.");
+ return NotFound();
}
- var user = _userManager.GetUserById(requestUserId);
-
- if (user is null)
+ if (!RequestHelpers.AssertCanUpdateUser(User, user, true))
{
- return NotFound("User not found");
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the password.");
}
if (request.ResetPassword)
@@ -386,7 +385,7 @@ public class UserController : BaseJellyfinApiController
return NotFound();
}
- if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, requestUserId, true))
+ if (!RequestHelpers.AssertCanUpdateUser(User, user, true))
{
return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed.");
}
@@ -396,7 +395,7 @@ public class UserController : BaseJellyfinApiController
await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false);
}
- await _userManager.UpdateConfigurationAsync(user.Id, updateUser.Configuration).ConfigureAwait(false);
+ await _userManager.UpdateConfigurationAsync(requestUserId, updateUser.Configuration).ConfigureAwait(false);
return NoContent();
}
@@ -495,7 +494,13 @@ public class UserController : BaseJellyfinApiController
[FromBody, Required] UserConfiguration userConfig)
{
var requestUserId = userId ?? User.GetUserId();
- if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, requestUserId, true))
+ var user = _userManager.GetUserById(requestUserId);
+ if (user is null)
+ {
+ return NotFound();
+ }
+
+ if (!RequestHelpers.AssertCanUpdateUser(User, user, true))
{
return StatusCode(StatusCodes.Status403Forbidden, "User configuration update not allowed");
}
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index 421f1bfb5..e7bf71727 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -560,7 +560,7 @@ public class UserLibraryController : BaseJellyfinApiController
IsPlayed = isPlayed,
Limit = limit,
ParentId = parentId ?? Guid.Empty,
- UserId = requestUserId,
+ User = user,
},
dtoOptions);
diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs
index 01da50d02..e24f78a88 100644
--- a/Jellyfin.Api/Controllers/UserViewsController.cs
+++ b/Jellyfin.Api/Controllers/UserViewsController.cs
@@ -8,6 +8,7 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.UserViewDtos;
using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -69,8 +70,9 @@ public class UserViewsController : BaseJellyfinApiController
[FromQuery] bool includeHidden = false)
{
userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(userId.Value) ?? throw new ResourceNotFoundException();
- var query = new UserViewQuery { UserId = userId.Value, IncludeHidden = includeHidden };
+ var query = new UserViewQuery { User = user, IncludeHidden = includeHidden };
if (includeExternalContent.HasValue)
{
@@ -87,8 +89,6 @@ public class UserViewsController : BaseJellyfinApiController
var dtoOptions = new DtoOptions().AddClientFields(User);
dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.PrimaryImageAspectRatio, ItemFields.DisplayPreferencesId];
- var user = _userManager.GetUserById(userId.Value);
-
var dtos = Array.ConvertAll(folders, i => _dtoService.GetBaseItemDto(i, dtoOptions, user));
return new QueryResult<BaseItemDto>(dtos);
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 7f9608378..effe7b021 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -267,7 +267,7 @@ public class VideosController : BaseJellyfinApiController
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="audioCodec">Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
@@ -299,7 +299,7 @@ public class VideosController : BaseJellyfinApiController
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -508,7 +508,7 @@ public class VideosController : BaseJellyfinApiController
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="audioCodec">Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
@@ -540,7 +540,7 @@ public class VideosController : BaseJellyfinApiController
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index 6f040cfae..ba92d811c 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -198,6 +198,17 @@ public class DynamicHlsHelper
AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
}
+ // Video rotation metadata is only supported in fMP4 remuxing
+ if (state.VideoStream is not null
+ && state.VideoRequest is not null
+ && (state.VideoStream?.Rotation ?? 0) != 0
+ && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+ && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
+ && !string.Equals(state.Request.SegmentContainer, "mp4", StringComparison.OrdinalIgnoreCase))
+ {
+ playlistUrl += "&AllowVideoStreamCopy=false";
+ }
+
var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
if (state.VideoStream is not null && state.VideoRequest is not null)
diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
index d0bfa1fbe..fa0db0541 100644
--- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
+++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
@@ -19,12 +19,12 @@ public static class HlsCodecStringHelpers
/// <summary>
/// Codec name for AC-3.
/// </summary>
- public const string AC3 = "mp4a.a5";
+ public const string AC3 = "ac-3";
/// <summary>
/// Codec name for E-AC-3.
/// </summary>
- public const string EAC3 = "mp4a.a6";
+ public const string EAC3 = "ec-3";
/// <summary>
/// Codec name for FLAC.
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index a3d7f471e..1d9c189a0 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -86,18 +86,17 @@ public static class RequestHelpers
/// <summary>
/// Checks if the user can update an entry.
/// </summary>
- /// <param name="userManager">An instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="claimsPrincipal">The <see cref="ClaimsPrincipal"/> for the current request.</param>
- /// <param name="userId">The user id.</param>
+ /// <param name="user">The user id.</param>
/// <param name="restrictUserPreferences">Whether to restrict the user preferences.</param>
/// <returns>A <see cref="bool"/> whether the user can update the entry.</returns>
- internal static bool AssertCanUpdateUser(IUserManager userManager, ClaimsPrincipal claimsPrincipal, Guid userId, bool restrictUserPreferences)
+ internal static bool AssertCanUpdateUser(ClaimsPrincipal claimsPrincipal, User user, bool restrictUserPreferences)
{
var authenticatedUserId = claimsPrincipal.GetUserId();
var isAdministrator = claimsPrincipal.IsInRole(UserRoles.Administrator);
// If they're going to update the record of another user, they must be an administrator
- if (!userId.Equals(authenticatedUserId) && !isAdministrator)
+ if (!user.Id.Equals(authenticatedUserId) && !isAdministrator)
{
return false;
}
@@ -108,12 +107,6 @@ public static class RequestHelpers
return true;
}
- var user = userManager.GetUserById(userId);
- if (user is null)
- {
- throw new ResourceNotFoundException();
- }
-
return user.EnableUserPreferenceAccess;
}
diff --git a/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs
index 78efacd94..53b5e3b7c 100644
--- a/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs
+++ b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs
@@ -24,6 +24,11 @@ public class LibraryOptionsResultDto
public IReadOnlyList<LibraryOptionInfoDto> SubtitleFetchers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
/// <summary>
+ /// Gets or sets the list of lyric fetchers.
+ /// </summary>
+ public IReadOnlyList<LibraryOptionInfoDto> LyricFetchers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
+
+ /// <summary>
/// Gets or sets the type options.
/// </summary>
public IReadOnlyList<LibraryTypeOptionsDto> TypeOptions { get; set; } = Array.Empty<LibraryTypeOptionsDto>();
diff --git a/Jellyfin.Data/Entities/MediaSegment.cs b/Jellyfin.Data/Entities/MediaSegment.cs
new file mode 100644
index 000000000..90120d772
--- /dev/null
+++ b/Jellyfin.Data/Entities/MediaSegment.cs
@@ -0,0 +1,42 @@
+using System;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Data.Enums;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// An entity representing the metadata for a group of trickplay tiles.
+/// </summary>
+public class MediaSegment
+{
+ /// <summary>
+ /// Gets or sets the id of the media segment.
+ /// </summary>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public Guid Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id of the associated item.
+ /// </summary>
+ public Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Type of content this segment defines.
+ /// </summary>
+ public MediaSegmentType Type { get; set; }
+
+ /// <summary>
+ /// Gets or sets the end of the segment.
+ /// </summary>
+ public long EndTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the start of the segment.
+ /// </summary>
+ public long StartTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets Id of the media segment provider this entry originates from.
+ /// </summary>
+ public required string SegmentProviderId { get; set; }
+}
diff --git a/Jellyfin.Data/Enums/MediaSegmentType.cs b/Jellyfin.Data/Enums/MediaSegmentType.cs
new file mode 100644
index 000000000..458635450
--- /dev/null
+++ b/Jellyfin.Data/Enums/MediaSegmentType.cs
@@ -0,0 +1,39 @@
+using Jellyfin.Data.Entities;
+
+namespace Jellyfin.Data.Enums;
+
+/// <summary>
+/// Defines the types of content an individual <see cref="MediaSegment"/> represents.
+/// </summary>
+public enum MediaSegmentType
+{
+ /// <summary>
+ /// Default media type or custom one.
+ /// </summary>
+ Unknown = 0,
+
+ /// <summary>
+ /// Commercial.
+ /// </summary>
+ Commercial = 1,
+
+ /// <summary>
+ /// Preview.
+ /// </summary>
+ Preview = 2,
+
+ /// <summary>
+ /// Recap.
+ /// </summary>
+ Recap = 3,
+
+ /// <summary>
+ /// Outro.
+ /// </summary>
+ Outro = 4,
+
+ /// <summary>
+ /// Intro.
+ /// </summary>
+ Intro = 5
+}
diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
index d8d1b6fa8..d7a46e2d5 100644
--- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
+++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
@@ -27,6 +27,8 @@ namespace Jellyfin.Server.Implementations.Devices
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IUserManager _userManager;
private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new();
+ private readonly ConcurrentDictionary<int, Device> _devices;
+ private readonly ConcurrentDictionary<string, DeviceOptions> _deviceOptions;
/// <summary>
/// Initializes a new instance of the <see cref="DeviceManager"/> class.
@@ -37,6 +39,23 @@ namespace Jellyfin.Server.Implementations.Devices
{
_dbProvider = dbProvider;
_userManager = userManager;
+ _devices = new ConcurrentDictionary<int, Device>();
+ _deviceOptions = new ConcurrentDictionary<string, DeviceOptions>();
+
+ using var dbContext = _dbProvider.CreateDbContext();
+ foreach (var device in dbContext.Devices
+ .OrderBy(d => d.Id)
+ .AsEnumerable())
+ {
+ _devices.TryAdd(device.Id, device);
+ }
+
+ foreach (var deviceOption in dbContext.DeviceOptions
+ .OrderBy(d => d.Id)
+ .AsEnumerable())
+ {
+ _deviceOptions.TryAdd(deviceOption.DeviceId, deviceOption);
+ }
}
/// <inheritdoc />
@@ -66,6 +85,8 @@ namespace Jellyfin.Server.Implementations.Devices
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
+ _deviceOptions[deviceId] = deviceOptions;
+
DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, deviceOptions)));
}
@@ -76,25 +97,17 @@ namespace Jellyfin.Server.Implementations.Devices
await using (dbContext.ConfigureAwait(false))
{
dbContext.Devices.Add(device);
-
await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ _devices.TryAdd(device.Id, device);
}
return device;
}
/// <inheritdoc />
- public async Task<DeviceOptions> GetDeviceOptions(string deviceId)
+ public DeviceOptions GetDeviceOptions(string deviceId)
{
- var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
- DeviceOptions? deviceOptions;
- await using (dbContext.ConfigureAwait(false))
- {
- deviceOptions = await dbContext.DeviceOptions
- .AsNoTracking()
- .FirstOrDefaultAsync(d => d.DeviceId == deviceId)
- .ConfigureAwait(false);
- }
+ _deviceOptions.TryGetValue(deviceId, out var deviceOptions);
return deviceOptions ?? new DeviceOptions(deviceId);
}
@@ -108,57 +121,43 @@ namespace Jellyfin.Server.Implementations.Devices
}
/// <inheritdoc />
- public async Task<DeviceInfo?> GetDevice(string id)
+ public DeviceInfo? GetDevice(string id)
{
- var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
- await using (dbContext.ConfigureAwait(false))
- {
- var device = await dbContext.Devices
- .Where(d => d.DeviceId == id)
- .OrderByDescending(d => d.DateLastActivity)
- .Include(d => d.User)
- .SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o })
- .FirstOrDefaultAsync()
- .ConfigureAwait(false);
+ var device = _devices.Values.Where(d => d.DeviceId == id).OrderByDescending(d => d.DateLastActivity).FirstOrDefault();
+ _deviceOptions.TryGetValue(id, out var deviceOption);
- var deviceInfo = device is null ? null : ToDeviceInfo(device.Device, device.Options);
-
- return deviceInfo;
- }
+ var deviceInfo = device is null ? null : ToDeviceInfo(device, deviceOption);
+ return deviceInfo;
}
/// <inheritdoc />
- public async Task<QueryResult<Device>> GetDevices(DeviceQuery query)
+ public QueryResult<Device> GetDevices(DeviceQuery query)
{
- var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
- await using (dbContext.ConfigureAwait(false))
+ IEnumerable<Device> devices = _devices.Values
+ .Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value))
+ .Where(device => query.DeviceId == null || device.DeviceId == query.DeviceId)
+ .Where(device => query.AccessToken == null || device.AccessToken == query.AccessToken)
+ .OrderBy(d => d.Id)
+ .ToList();
+ var count = devices.Count();
+
+ if (query.Skip.HasValue)
{
- var devices = dbContext.Devices
- .OrderBy(d => d.Id)
- .Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value))
- .Where(device => query.DeviceId == null || device.DeviceId == query.DeviceId)
- .Where(device => query.AccessToken == null || device.AccessToken == query.AccessToken);
-
- var count = await devices.CountAsync().ConfigureAwait(false);
-
- if (query.Skip.HasValue)
- {
- devices = devices.Skip(query.Skip.Value);
- }
-
- if (query.Limit.HasValue)
- {
- devices = devices.Take(query.Limit.Value);
- }
+ devices = devices.Skip(query.Skip.Value);
+ }
- return new QueryResult<Device>(query.Skip, count, await devices.ToListAsync().ConfigureAwait(false));
+ if (query.Limit.HasValue)
+ {
+ devices = devices.Take(query.Limit.Value);
}
+
+ return new QueryResult<Device>(query.Skip, count, devices.ToList());
}
/// <inheritdoc />
- public async Task<QueryResult<DeviceInfo>> GetDeviceInfos(DeviceQuery query)
+ public QueryResult<DeviceInfo> GetDeviceInfos(DeviceQuery query)
{
- var devices = await GetDevices(query).ConfigureAwait(false);
+ var devices = GetDevices(query);
return new QueryResult<DeviceInfo>(
devices.StartIndex,
@@ -167,38 +166,36 @@ namespace Jellyfin.Server.Implementations.Devices
}
/// <inheritdoc />
- public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId)
+ public QueryResult<DeviceInfo> GetDevicesForUser(Guid? userId)
{
- var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
- await using (dbContext.ConfigureAwait(false))
+ IEnumerable<Device> devices = _devices.Values
+ .OrderByDescending(d => d.DateLastActivity)
+ .ThenBy(d => d.DeviceId);
+
+ if (!userId.IsNullOrEmpty())
{
- var sessions = dbContext.Devices
- .Include(d => d.User)
- .OrderByDescending(d => d.DateLastActivity)
- .ThenBy(d => d.DeviceId)
- .SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o })
- .AsAsyncEnumerable();
-
- if (!userId.IsNullOrEmpty())
+ var user = _userManager.GetUserById(userId.Value);
+ if (user is null)
{
- var user = _userManager.GetUserById(userId.Value);
- if (user is null)
- {
- throw new ResourceNotFoundException();
- }
-
- sessions = sessions.Where(i => CanAccessDevice(user, i.Device.DeviceId));
+ throw new ResourceNotFoundException();
}
- var array = await sessions.Select(device => ToDeviceInfo(device.Device, device.Options)).ToArrayAsync().ConfigureAwait(false);
-
- return new QueryResult<DeviceInfo>(array);
+ devices = devices.Where(i => CanAccessDevice(user, i.DeviceId));
}
+
+ var array = devices.Select(device =>
+ {
+ _deviceOptions.TryGetValue(device.DeviceId, out var option);
+ return ToDeviceInfo(device, option);
+ }).ToArray();
+
+ return new QueryResult<DeviceInfo>(array);
}
/// <inheritdoc />
public async Task DeleteDevice(Device device)
{
+ _devices.TryRemove(device.Id, out _);
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
@@ -208,6 +205,19 @@ namespace Jellyfin.Server.Implementations.Devices
}
/// <inheritdoc />
+ public async Task UpdateDevice(Device device)
+ {
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ dbContext.Devices.Update(device);
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
+
+ _devices[device.Id] = device;
+ }
+
+ /// <inheritdoc />
public bool CanAccessDevice(User user, string deviceId)
{
ArgumentNullException.ThrowIfNull(user);
@@ -225,6 +235,11 @@ namespace Jellyfin.Server.Implementations.Devices
private DeviceInfo ToDeviceInfo(Device authInfo, DeviceOptions? options = null)
{
var caps = GetCapabilities(authInfo.DeviceId);
+ var user = _userManager.GetUserById(authInfo.UserId);
+ if (user is null)
+ {
+ throw new ResourceNotFoundException("User with UserId " + authInfo.UserId + " not found");
+ }
return new DeviceInfo
{
@@ -232,7 +247,7 @@ namespace Jellyfin.Server.Implementations.Devices
AppVersion = authInfo.AppVersion,
Id = authInfo.DeviceId,
LastUserId = authInfo.UserId,
- LastUserName = authInfo.User.Username,
+ LastUserName = user.Username,
Name = authInfo.DeviceName,
DateLastActivity = authInfo.DateLastActivity,
IconUrl = caps.IconUrl,
diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
index a88989840..ff29d69b4 100644
--- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
+++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
@@ -1,6 +1,5 @@
using System;
using System.IO;
-using EFCoreSecondLevelCacheInterceptor;
using MediaBrowser.Common.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
@@ -16,28 +15,13 @@ public static class ServiceCollectionExtensions
/// Adds the <see cref="IDbContextFactory{TContext}"/> interface to the service collection with second level caching enabled.
/// </summary>
/// <param name="serviceCollection">An instance of the <see cref="IServiceCollection"/> interface.</param>
- /// <param name="disableSecondLevelCache">Whether second level cache disabled..</param>
/// <returns>The updated service collection.</returns>
- public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection, bool disableSecondLevelCache)
+ public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection)
{
- if (!disableSecondLevelCache)
- {
- serviceCollection.AddEFSecondLevelCache(options =>
- options.UseMemoryCacheProvider()
- .CacheAllQueries(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(10))
- .UseCacheKeyPrefix("EF_")
- // Don't cache null values. Remove this optional setting if it's not necessary.
- .SkipCachingResults(result => result.Value is null or EFTableRows { RowsCount: 0 }));
- }
-
serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) =>
{
var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>();
- var dbOpt = opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
- if (!disableSecondLevelCache)
- {
- dbOpt.AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>());
- }
+ opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
});
return serviceCollection;
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index 7c4155bfc..20944ee4b 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -27,7 +27,6 @@
<ItemGroup>
<PackageReference Include="AsyncKeyedLock" />
- <PackageReference Include="EFCoreSecondLevelCacheInterceptor" />
<PackageReference Include="System.Linq.Async" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
index ea99af004..150bc8bb4 100644
--- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
@@ -83,6 +83,11 @@ public class JellyfinDbContext : DbContext
/// </summary>
public DbSet<TrickplayInfo> TrickplayInfos => Set<TrickplayInfo>();
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the media segments.
+ /// </summary>
+ public DbSet<MediaSegment> MediaSegments => Set<MediaSegment>();
+
/*public DbSet<Artwork> Artwork => Set<Artwork>();
public DbSet<Book> Books => Set<Book>();
diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
new file mode 100644
index 000000000..7916d15c9
--- /dev/null
+++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.MediaSegments;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Server.Implementations.MediaSegments;
+
+/// <summary>
+/// Manages media segments retrival and storage.
+/// </summary>
+public class MediaSegmentManager : IMediaSegmentManager
+{
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MediaSegmentManager"/> class.
+ /// </summary>
+ /// <param name="dbProvider">EFCore Database factory.</param>
+ public MediaSegmentManager(IDbContextFactory<JellyfinDbContext> dbProvider)
+ {
+ _dbProvider = dbProvider;
+ }
+
+ /// <inheritdoc />
+ public async Task<MediaSegmentDto> CreateSegmentAsync(MediaSegmentDto mediaSegment, string segmentProviderId)
+ {
+ ArgumentOutOfRangeException.ThrowIfLessThan(mediaSegment.EndTicks, mediaSegment.StartTicks);
+
+ using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ db.MediaSegments.Add(Map(mediaSegment, segmentProviderId));
+ await db.SaveChangesAsync().ConfigureAwait(false);
+ return mediaSegment;
+ }
+
+ /// <inheritdoc />
+ public async Task DeleteSegmentAsync(Guid segmentId)
+ {
+ using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await db.MediaSegments.Where(e => e.Id.Equals(segmentId)).ExecuteDeleteAsync().ConfigureAwait(false);
+ }
+
+ /// <inheritdoc />
+ public async Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter)
+ {
+ using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+
+ var query = db.MediaSegments
+ .Where(e => e.ItemId.Equals(itemId));
+
+ if (typeFilter is not null)
+ {
+ query = query.Where(e => typeFilter.Contains(e.Type));
+ }
+
+ return query
+ .OrderBy(e => e.StartTicks)
+ .AsNoTracking()
+ .ToImmutableList()
+ .Select(Map);
+ }
+
+ private static MediaSegmentDto Map(MediaSegment segment)
+ {
+ return new MediaSegmentDto()
+ {
+ Id = segment.Id,
+ EndTicks = segment.EndTicks,
+ ItemId = segment.ItemId,
+ StartTicks = segment.StartTicks,
+ Type = segment.Type
+ };
+ }
+
+ private static MediaSegment Map(MediaSegmentDto segment, string segmentProviderId)
+ {
+ return new MediaSegment()
+ {
+ Id = segment.Id,
+ EndTicks = segment.EndTicks,
+ ItemId = segment.ItemId,
+ StartTicks = segment.StartTicks,
+ Type = segment.Type,
+ SegmentProviderId = segmentProviderId
+ };
+ }
+
+ /// <inheritdoc />
+ public bool HasSegments(Guid itemId)
+ {
+ using var db = _dbProvider.CreateDbContext();
+ return db.MediaSegments.Any(e => e.ItemId.Equals(itemId));
+ }
+
+ /// <inheritdoc/>
+ public bool IsTypeSupported(BaseItem baseItem)
+ {
+ return baseItem.MediaType is Data.Enums.MediaType.Video or Data.Enums.MediaType.Audio;
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20240729140605_AddMediaSegments.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20240729140605_AddMediaSegments.Designer.cs
new file mode 100644
index 000000000..c03cb4760
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20240729140605_AddMediaSegments.Designer.cs
@@ -0,0 +1,708 @@
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20240729140605_AddMediaSegments")]
+ partial class AddMediaSegments
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.7");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("EndTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaSegments");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20240729140605_AddMediaSegments.cs b/Jellyfin.Server.Implementations/Migrations/20240729140605_AddMediaSegments.cs
new file mode 100644
index 000000000..24a8ffc42
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20240729140605_AddMediaSegments.cs
@@ -0,0 +1,38 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class AddMediaSegments : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "MediaSegments",
+ columns: table => new
+ {
+ Id = table.Column<Guid>(type: "TEXT", nullable: false),
+ ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+ Type = table.Column<int>(type: "INTEGER", nullable: false),
+ EndTicks = table.Column<long>(type: "INTEGER", nullable: false),
+ StartTicks = table.Column<long>(type: "INTEGER", nullable: false),
+ SegmentProviderId = table.Column<string>(type: "TEXT", nullable: false),
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_MediaSegments", x => x.Id);
+ });
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "MediaSegments");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index f725ababe..cdeeb6d87 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -1,4 +1,4 @@
-// <auto-generated />
+// <auto-generated />
using System;
using Jellyfin.Server.Implementations;
using Microsoft.EntityFrameworkCore;
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "7.0.11");
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.7");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@@ -270,6 +270,32 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("ItemDisplayPreferences");
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("EndTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SegmentProviderId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaSegments");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.Property<int>("Id")
diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
index 6bda12c5b..2ae722982 100644
--- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
+++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
@@ -4,7 +4,10 @@ using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
+using Jellyfin.Data.Queries;
+using Jellyfin.Extensions;
using MediaBrowser.Controller;
+using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Http;
@@ -17,15 +20,18 @@ namespace Jellyfin.Server.Implementations.Security
{
private readonly IDbContextFactory<JellyfinDbContext> _jellyfinDbProvider;
private readonly IUserManager _userManager;
+ private readonly IDeviceManager _deviceManager;
private readonly IServerApplicationHost _serverApplicationHost;
public AuthorizationContext(
IDbContextFactory<JellyfinDbContext> jellyfinDb,
IUserManager userManager,
+ IDeviceManager deviceManager,
IServerApplicationHost serverApplicationHost)
{
_jellyfinDbProvider = jellyfinDb;
_userManager = userManager;
+ _deviceManager = deviceManager;
_serverApplicationHost = serverApplicationHost;
}
@@ -121,7 +127,11 @@ namespace Jellyfin.Server.Implementations.Security
var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
- var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false);
+ var device = _deviceManager.GetDevices(
+ new DeviceQuery
+ {
+ AccessToken = token
+ }).Items.FirstOrDefault();
if (device is not null)
{
@@ -178,8 +188,7 @@ namespace Jellyfin.Server.Implementations.Security
if (updateToken)
{
- dbContext.Devices.Update(device);
- await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ await _deviceManager.UpdateDevice(device).ConfigureAwait(false);
}
}
else
diff --git a/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs b/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs
index e40b541a3..634aea9f0 100644
--- a/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs
+++ b/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs
@@ -60,10 +60,10 @@ public sealed class DeviceAccessHost : IHostedService
private async Task UpdateDeviceAccess(User user)
{
- var existing = (await _deviceManager.GetDevices(new DeviceQuery
+ var existing = _deviceManager.GetDevices(new DeviceQuery
{
UserId = user.Id
- }).ConfigureAwait(false)).Items;
+ }).Items;
foreach (var device in existing)
{
diff --git a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs
index 858df6728..6b95770ed 100644
--- a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs
@@ -85,6 +85,6 @@ public static class WebHostBuilderExtensions
logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath);
}
})
- .UseStartup(_ => new Startup(appHost, startupConfig));
+ .UseStartup(_ => new Startup(appHost));
}
}
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 2ff377403..e9fb3e4c2 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -40,18 +40,15 @@ namespace Jellyfin.Server
{
private readonly CoreAppHost _serverApplicationHost;
private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly IConfiguration _startupConfig;
/// <summary>
/// Initializes a new instance of the <see cref="Startup" /> class.
/// </summary>
/// <param name="appHost">The server application host.</param>
- /// <param name="startupConfig">The server startupConfig.</param>
- public Startup(CoreAppHost appHost, IConfiguration startupConfig)
+ public Startup(CoreAppHost appHost)
{
_serverApplicationHost = appHost;
_serverConfigurationManager = appHost.ConfigurationManager;
- _startupConfig = startupConfig;
}
/// <summary>
@@ -70,7 +67,7 @@ namespace Jellyfin.Server
// TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
- services.AddJellyfinDbContext(_startupConfig.GetSqliteSecondLevelCacheDisabled());
+ services.AddJellyfinDbContext();
services.AddJellyfinApiSwagger();
// configure custom legacy authentication
diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs
index eb181dcc4..5566421cb 100644
--- a/MediaBrowser.Controller/Devices/IDeviceManager.cs
+++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs
@@ -44,26 +44,28 @@ namespace MediaBrowser.Controller.Devices
/// </summary>
/// <param name="id">The identifier.</param>
/// <returns>DeviceInfo.</returns>
- Task<DeviceInfo> GetDevice(string id);
+ DeviceInfo GetDevice(string id);
/// <summary>
/// Gets devices based on the provided query.
/// </summary>
/// <param name="query">The device query.</param>
/// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the devices.</returns>
- Task<QueryResult<Device>> GetDevices(DeviceQuery query);
+ QueryResult<Device> GetDevices(DeviceQuery query);
- Task<QueryResult<DeviceInfo>> GetDeviceInfos(DeviceQuery query);
+ QueryResult<DeviceInfo> GetDeviceInfos(DeviceQuery query);
/// <summary>
/// Gets the devices.
/// </summary>
/// <param name="userId">The user's id, or <c>null</c>.</param>
/// <returns>IEnumerable&lt;DeviceInfo&gt;.</returns>
- Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId);
+ QueryResult<DeviceInfo> GetDevicesForUser(Guid? userId);
Task DeleteDevice(Device device);
+ Task UpdateDevice(Device device);
+
/// <summary>
/// Determines whether this instance [can access device] the specified user identifier.
/// </summary>
@@ -74,6 +76,6 @@ namespace MediaBrowser.Controller.Devices
Task UpdateDeviceOptions(string deviceId, string deviceName);
- Task<DeviceOptions> GetDeviceOptions(string deviceId);
+ DeviceOptions GetDeviceOptions(string deviceId);
}
}
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 7b6f364f7..125f8f225 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -47,7 +47,7 @@ namespace MediaBrowser.Controller.Entities
/// The supported image extensions.
/// </summary>
public static readonly string[] SupportedImageExtensions
- = new[] { ".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif" };
+ = new[] { ".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif", ".svg" };
private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions)
{
@@ -487,6 +487,8 @@ namespace MediaBrowser.Controller.Entities
public static IMediaSourceManager MediaSourceManager { get; set; }
+ public static IMediaSegmentManager MediaSegmentManager { get; set; }
+
/// <summary>
/// Gets or sets the name of the forced sort.
/// </summary>
@@ -1116,7 +1118,10 @@ namespace MediaBrowser.Controller.Entities
RunTimeTicks = item.RunTimeTicks,
Container = item.Container,
Size = item.Size,
- Type = type
+ Type = type,
+ HasSegments = MediaSegmentManager.IsTypeSupported(item)
+ && (protocol is null or MediaProtocol.File)
+ && MediaSegmentManager.HasSegments(item.Id)
};
if (string.IsNullOrEmpty(info.Path))
@@ -1839,7 +1844,7 @@ namespace MediaBrowser.Controller.Entities
data.LastPlayedDate = datePlayed ?? data.LastPlayedDate ?? DateTime.UtcNow;
data.Played = true;
- UserDataManager.SaveUserData(user.Id, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
+ UserDataManager.SaveUserData(user, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
}
/// <summary>
@@ -1861,7 +1866,7 @@ namespace MediaBrowser.Controller.Entities
data.LastPlayedDate = null;
data.Played = false;
- UserDataManager.SaveUserData(user.Id, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
+ UserDataManager.SaveUserData(user, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
}
/// <summary>
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
index d7ccfd8ae..a07187d2f 100644
--- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -112,37 +112,31 @@ namespace MediaBrowser.Controller.Entities.Movies
return true;
}
- public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
+ private IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user)
{
- var children = base.GetChildren(user, includeLinkedChildren, query);
-
- if (string.Equals(DisplayOrder, "SortName", StringComparison.OrdinalIgnoreCase))
+ if (!Enum.TryParse<ItemSortBy>(DisplayOrder, out var sortBy))
{
- // Sort by name
- return LibraryManager.Sort(children, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
+ sortBy = ItemSortBy.PremiereDate;
}
- if (string.Equals(DisplayOrder, "PremiereDate", StringComparison.OrdinalIgnoreCase))
+ if (sortBy == ItemSortBy.Default)
{
- // Sort by release date
- return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList();
+ return items;
}
- // Default sorting
- return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList();
+ return LibraryManager.Sort(items, user, new[] { sortBy }, SortOrder.Ascending);
+ }
+
+ public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
+ {
+ var children = base.GetChildren(user, includeLinkedChildren, query);
+ return Sort(children, user).ToList();
}
public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
{
var children = base.GetRecursiveChildren(user, query);
-
- if (string.Equals(DisplayOrder, "PremiereDate", StringComparison.OrdinalIgnoreCase))
- {
- // Sort by release date
- return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList();
- }
-
- return children;
+ return Sort(children, user).ToList();
}
public BoxSetInfo GetLookupInfo()
diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs
index 083f12746..181b9be2b 100644
--- a/MediaBrowser.Controller/Entities/TV/Season.cs
+++ b/MediaBrowser.Controller/Entities/TV/Season.cs
@@ -238,6 +238,7 @@ namespace MediaBrowser.Controller.Entities.TV
if (series is not null)
{
id.SeriesProviderIds = series.ProviderIds;
+ id.SeriesDisplayOrder = series.DisplayOrder;
}
return id;
diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs
index fc8a29763..a687adedd 100644
--- a/MediaBrowser.Controller/Entities/UserRootFolder.cs
+++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs
@@ -76,7 +76,7 @@ namespace MediaBrowser.Controller.Entities
var result = UserViewManager.GetUserViews(new UserViewQuery
{
- UserId = query.User.Id,
+ User = query.User,
PresetViews = query.PresetViews
});
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index 3a1d0c070..2fda7ee6f 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -337,7 +337,7 @@ namespace MediaBrowser.Controller.Entities
{
Limit = query.Limit,
StartIndex = query.StartIndex,
- UserId = query.User.Id
+ User = query.User
},
parentFolders,
query.DtoOptions);
diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
index 7dfda73bf..0aaf4fcd9 100644
--- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
+++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
@@ -30,6 +30,11 @@ namespace MediaBrowser.Controller.Extensions
public const string FfmpegProbeSizeKey = "FFmpeg:probesize";
/// <summary>
+ /// The key for the skipping FFmpeg validation.
+ /// </summary>
+ public const string FfmpegSkipValidationKey = "FFmpeg:novalidation";
+
+ /// <summary>
/// The key for the FFmpeg analyze duration option.
/// </summary>
public const string FfmpegAnalyzeDurationKey = "FFmpeg:analyzeduration";
@@ -65,11 +70,6 @@ namespace MediaBrowser.Controller.Extensions
public const string SqliteCacheSizeKey = "sqlite:cacheSize";
/// <summary>
- /// Disable second level cache of sqlite.
- /// </summary>
- public const string SqliteDisableSecondLevelCacheKey = "sqlite:disableSecondLevelCache";
-
- /// <summary>
/// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>.
/// </summary>
/// <param name="configuration">The configuration to retrieve the value from.</param>
@@ -95,6 +95,14 @@ namespace MediaBrowser.Controller.Extensions
=> configuration[FfmpegAnalyzeDurationKey];
/// <summary>
+ /// Gets a value indicating whether the server should validate FFmpeg during startup.
+ /// </summary>
+ /// <param name="configuration">The configuration to read the setting from.</param>
+ /// <returns><c>true</c> if the server should validate FFmpeg during startup, otherwise <c>false</c>.</returns>
+ public static bool GetFFmpegSkipValidation(this IConfiguration configuration)
+ => configuration.GetValue<bool>(FfmpegSkipValidationKey);
+
+ /// <summary>
/// Gets a value indicating whether playlists should allow duplicate entries from the <see cref="IConfiguration"/>.
/// </summary>
/// <param name="configuration">The configuration to read the setting from.</param>
@@ -133,15 +141,5 @@ namespace MediaBrowser.Controller.Extensions
/// <returns>The sqlite cache size.</returns>
public static int? GetSqliteCacheSize(this IConfiguration configuration)
=> configuration.GetValue<int?>(SqliteCacheSizeKey);
-
- /// <summary>
- /// Gets whether second level cache disabled from the <see cref="IConfiguration" />.
- /// </summary>
- /// <param name="configuration">The configuration to read the setting from.</param>
- /// <returns>Whether second level cache disabled.</returns>
- public static bool GetSqliteSecondLevelCacheDisabled(this IConfiguration configuration)
- {
- return configuration.GetValue<bool>(SqliteDisableSecondLevelCacheKey);
- }
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 7a1067dcc..a459ce8b6 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -64,6 +64,10 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1);
private readonly Version _minFFmpegVaapiH26xEncA53CcSei = new Version(6, 0);
private readonly Version _minFFmpegReadrateOption = new Version(5, 0);
+ private readonly Version _minFFmpegWorkingVtHwSurface = new Version(7, 0, 1);
+ private readonly Version _minFFmpegDisplayRotationOption = new Version(6, 0);
+ private readonly Version _minFFmpegAdvancedTonemapMode = new Version(7, 0, 1);
+ private readonly Version _minFFmpegAlteredVaVkInterop = new Version(7, 0, 1);
private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled);
@@ -102,11 +106,13 @@ namespace MediaBrowser.Controller.MediaEncoding
"m4v",
};
+ private static readonly string[] _legacyTonemapModes = new[] { "max", "rgb" };
+ private static readonly string[] _advancedTonemapModes = new[] { "lum", "itp" };
+
// Set max transcoding channels for encoders that can't handle more than a set amount of channels
// AAC, FLAC, ALAC, libopus, libvorbis encoders all support at least 8 channels
private static readonly Dictionary<string, int> _audioTranscodeChannelLookup = new(StringComparer.OrdinalIgnoreCase)
{
- { "wmav2", 2 },
{ "libmp3lame", 2 },
{ "libfdk_aac", 6 },
{ "ac3", 6 },
@@ -231,6 +237,7 @@ namespace MediaBrowser.Controller.MediaEncoding
&& _mediaEncoder.SupportsFilter("tonemap_vaapi")
&& _mediaEncoder.SupportsFilter("procamp_vaapi")
&& _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayVaapiFrameSync)
+ && _mediaEncoder.SupportsFilter("transpose_vaapi")
&& _mediaEncoder.SupportsFilter("hwupload_vaapi");
}
@@ -248,6 +255,8 @@ namespace MediaBrowser.Controller.MediaEncoding
&& _mediaEncoder.SupportsFilter("scale_opencl")
&& _mediaEncoder.SupportsFilterWithOption(FilterOptionType.TonemapOpenclBt2390)
&& _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayOpenclFrameSync);
+
+ // Let transpose_opencl optional for the time being.
}
private bool IsCudaFullSupported()
@@ -258,6 +267,8 @@ namespace MediaBrowser.Controller.MediaEncoding
&& _mediaEncoder.SupportsFilterWithOption(FilterOptionType.TonemapCudaName)
&& _mediaEncoder.SupportsFilter("overlay_cuda")
&& _mediaEncoder.SupportsFilter("hwupload_cuda");
+
+ // Let transpose_cuda optional for the time being.
}
private bool IsVulkanFullSupported()
@@ -265,7 +276,9 @@ namespace MediaBrowser.Controller.MediaEncoding
return _mediaEncoder.SupportsHwaccel("vulkan")
&& _mediaEncoder.SupportsFilter("libplacebo")
&& _mediaEncoder.SupportsFilter("scale_vulkan")
- && _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayVulkanFrameSync);
+ && _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayVulkanFrameSync)
+ && _mediaEncoder.SupportsFilter("transpose_vulkan")
+ && _mediaEncoder.SupportsFilter("flip_vulkan");
}
private bool IsVideoToolboxFullSupported()
@@ -275,6 +288,8 @@ namespace MediaBrowser.Controller.MediaEncoding
&& _mediaEncoder.SupportsFilter("overlay_videotoolbox")
&& _mediaEncoder.SupportsFilter("tonemap_videotoolbox")
&& _mediaEncoder.SupportsFilter("scale_vt");
+
+ // Let transpose_vt optional for the time being.
}
private bool IsSwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
@@ -282,14 +297,12 @@ namespace MediaBrowser.Controller.MediaEncoding
if (state.VideoStream is null
|| !options.EnableTonemapping
|| GetVideoColorBitDepth(state) != 10
- || !_mediaEncoder.SupportsFilter("tonemapx")
- || !(string.Equals(state.VideoStream?.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase) || string.Equals(state.VideoStream?.ColorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase)))
+ || !_mediaEncoder.SupportsFilter("tonemapx"))
{
return false;
}
- return state.VideoStream.VideoRange == VideoRange.HDR
- && state.VideoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG;
+ return state.VideoStream.VideoRange == VideoRange.HDR;
}
private bool IsHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
@@ -399,27 +412,6 @@ namespace MediaBrowser.Controller.MediaEncoding
return GetMjpegEncoder(state, encodingOptions);
}
- if (string.Equals(codec, "vp8", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "vpx", StringComparison.OrdinalIgnoreCase))
- {
- return "libvpx";
- }
-
- if (string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
- {
- return "libvpx-vp9";
- }
-
- if (string.Equals(codec, "wmv", StringComparison.OrdinalIgnoreCase))
- {
- return "wmv2";
- }
-
- if (string.Equals(codec, "theora", StringComparison.OrdinalIgnoreCase))
- {
- return "libtheora";
- }
-
if (_validationRegex.IsMatch(codec))
{
return codec.ToLowerInvariant();
@@ -739,11 +731,6 @@ namespace MediaBrowser.Controller.MediaEncoding
return "libvorbis";
}
- if (string.Equals(codec, "wma", StringComparison.OrdinalIgnoreCase))
- {
- return "wmav2";
- }
-
if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase))
{
return "libopus";
@@ -1179,9 +1166,6 @@ namespace MediaBrowser.Controller.MediaEncoding
args.Append(vidDecoder);
}
- // hw transpose filters should be added manually.
- args.Append(" -noautorotate");
-
return args.ToString().Trim();
}
@@ -1340,7 +1324,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// Apply aac_adtstoasc bitstream filter when media source is in mpegts.
if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)
- && (string.Equals(mediaSourceContainer, "mpegts", StringComparison.OrdinalIgnoreCase)
+ && (string.Equals(mediaSourceContainer, "ts", StringComparison.OrdinalIgnoreCase)
|| string.Equals(mediaSourceContainer, "aac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase)))
{
@@ -1379,20 +1363,6 @@ namespace MediaBrowser.Controller.MediaEncoding
// Currently use the same buffer size for all encoders
int bufsize = bitrate * 2;
- if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoCodec, "libvpx-vp9", StringComparison.OrdinalIgnoreCase))
- {
- // When crf is used with vpx, b:v becomes a max rate
- // https://trac.ffmpeg.org/wiki/Encode/VP8
- // https://trac.ffmpeg.org/wiki/Encode/VP9
- return FormattableString.Invariant($" -maxrate:v {bitrate} -bufsize:v {bufsize} -b:v {bitrate}");
- }
-
- if (string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase))
- {
- return FormattableString.Invariant($" -b:v {bitrate}");
- }
-
if (string.Equals(videoCodec, "libsvtav1", StringComparison.OrdinalIgnoreCase))
{
return FormattableString.Invariant($" -b:v {bitrate} -bufsize {bufsize}");
@@ -1519,7 +1489,6 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
- // TODO: Perhaps also use original_size=1920x800 ??
return string.Format(
CultureInfo.InvariantCulture,
"subtitles=f='{0}'{1}{2}{3}{4}{5}",
@@ -1541,7 +1510,6 @@ namespace MediaBrowser.Controller.MediaEncoding
alphaParam,
sub2videoParam,
fontParam,
- // fallbackFontParam,
setPtsParam);
}
@@ -1815,12 +1783,6 @@ namespace MediaBrowser.Controller.MediaEncoding
{
param += " -preset veryfast";
}
-
- // Only h264_qsv has look_ahead option
- if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase))
- {
- param += " -look_ahead 0";
- }
}
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc)
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) // hevc (hevc_nvenc)
@@ -1928,93 +1890,6 @@ namespace MediaBrowser.Controller.MediaEncoding
break;
}
}
- else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // vp8
- {
- // Values 0-3, 0 being highest quality but slower
- var profileScore = 0;
-
- var qmin = "0";
- var qmax = "50";
- var crf = "10";
-
- if (isVc1)
- {
- profileScore++;
- }
-
- // Max of 2
- profileScore = Math.Min(profileScore, 2);
-
- // http://www.webmproject.org/docs/encoder-parameters/
- param += string.Format(
- CultureInfo.InvariantCulture,
- " -speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
- profileScore.ToString(CultureInfo.InvariantCulture),
- crf,
- qmin,
- qmax);
- }
- else if (string.Equals(videoEncoder, "libvpx-vp9", StringComparison.OrdinalIgnoreCase)) // vp9
- {
- // When `-deadline` is set to `good` or `best`, `-cpu-used` ranges from 0-5.
- // When `-deadline` is set to `realtime`, `-cpu-used` ranges from 0-15.
- // Resources:
- // * https://trac.ffmpeg.org/wiki/Encode/VP9
- // * https://superuser.com/questions/1586934
- // * https://developers.google.com/media/vp9
- param += encodingOptions.EncoderPreset switch
- {
- "veryslow" => " -deadline best -cpu-used 0",
- "slower" => " -deadline best -cpu-used 2",
- "slow" => " -deadline best -cpu-used 3",
- "medium" => " -deadline good -cpu-used 0",
- "fast" => " -deadline good -cpu-used 1",
- "faster" => " -deadline good -cpu-used 2",
- "veryfast" => " -deadline good -cpu-used 3",
- "superfast" => " -deadline good -cpu-used 4",
- "ultrafast" => " -deadline good -cpu-used 5",
- _ => " -deadline good -cpu-used 1"
- };
-
- // TODO: until VP9 gets its own CRF setting, base CRF on H.265.
- int h265Crf = encodingOptions.H265Crf;
- int defaultVp9Crf = 31;
- if (h265Crf >= 0 && h265Crf <= 51)
- {
- // This conversion factor is chosen to match the default CRF for H.265 to the
- // recommended 1080p CRF from Google. The factor also maps the logarithmic CRF
- // scale of x265 [0, 51] to that of VP9 [0, 63] relatively well.
-
- // Resources:
- // * https://developers.google.com/media/vp9/settings/vod
- const float H265ToVp9CrfConversionFactor = 1.12F;
-
- var vp9Crf = Convert.ToInt32(h265Crf * H265ToVp9CrfConversionFactor);
-
- // Encoder allows for CRF values in the range [0, 63].
- vp9Crf = Math.Clamp(vp9Crf, 0, 63);
-
- param += FormattableString.Invariant($" -crf {vp9Crf}");
- }
- else
- {
- param += FormattableString.Invariant($" -crf {defaultVp9Crf}");
- }
-
- param += " -row-mt 1 -profile 1";
- }
- else if (string.Equals(videoEncoder, "mpeg4", StringComparison.OrdinalIgnoreCase))
- {
- param += " -mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2";
- }
- else if (string.Equals(videoEncoder, "wmv2", StringComparison.OrdinalIgnoreCase)) // asf/wmv
- {
- param += " -qmin 2";
- }
- else if (string.Equals(videoEncoder, "msmpeg4", StringComparison.OrdinalIgnoreCase))
- {
- param += " -mbd 2";
- }
param += GetVideoBitrateParam(state, videoEncoder);
@@ -2194,7 +2069,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
{
- param += " -x264opts:0 subme=0:me_range=4:rc_lookahead=10:me=dia:no_chroma_me:8x8dct=0:partitions=none";
+ param += " -x264opts:0 subme=0:me_range=16:rc_lookahead=10:me=hex:open_gop=0";
}
if (string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
@@ -2202,8 +2077,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// libx265 only accept level option in -x265-params.
// level option may cause libx265 to fail.
// libx265 cannot adjust the given level, just throw an error.
- // TODO: set fine tuned params.
- param += " -x265-params:0 no-info=1";
+ param += " -x265-params:0 subme=3:merange=25:rc-lookahead=10:me=star:ctu=32:max-tu-size=32:min-cu-size=16:rskip=2:rskip-edge-threshold=2:no-sao=1:no-strong-intra-smoothing=1:no-scenecut=1:no-open-gop=1:no-info=1";
}
if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase)
@@ -3080,8 +2954,10 @@ namespace MediaBrowser.Controller.MediaEncoding
}
public static string GetHwScaleFilter(
+ string hwScalePrefix,
string hwScaleSuffix,
string videoFormat,
+ bool swapOutputWandH,
int? videoWidth,
int? videoHeight,
int? requestedWidth,
@@ -3103,8 +2979,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|| !videoHeight.HasValue
|| outHeight.Value != videoHeight.Value;
- var arg1 = isSizeFixed ? ("=w=" + outWidth.Value + ":h=" + outHeight.Value) : string.Empty;
- var arg2 = isFormatFixed ? ("format=" + videoFormat) : string.Empty;
+ var swpOutW = swapOutputWandH ? outHeight.Value : outWidth.Value;
+ var swpOutH = swapOutputWandH ? outWidth.Value : outHeight.Value;
+
+ var arg1 = isSizeFixed ? $"=w={swpOutW}:h={swpOutH}" : string.Empty;
+ var arg2 = isFormatFixed ? $"format={videoFormat}" : string.Empty;
if (isFormatFixed)
{
arg2 = (isSizeFixed ? ':' : '=') + arg2;
@@ -3114,7 +2993,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
return string.Format(
CultureInfo.InvariantCulture,
- "scale_{0}{1}{2}",
+ "{0}_{1}{2}{3}",
+ hwScalePrefix ?? "scale",
hwScaleSuffix,
arg1,
arg2);
@@ -3143,7 +3023,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
return string.Format(
CultureInfo.InvariantCulture,
- @"scale=-1:{1}:fast_bilinear,scale,crop,pad=max({0}\,iw):max({1}\,ih):(ow-iw)/2:(oh-ih)/2:black@0,crop={0}:{1}",
+ @"scale,scale=-1:{1}:fast_bilinear,crop,pad=max({0}\,iw):max({1}\,ih):(ow-iw)/2:(oh-ih)/2:black@0,crop={0}:{1}",
outWidth.Value,
outHeight.Value);
}
@@ -3414,13 +3294,15 @@ namespace MediaBrowser.Controller.MediaEncoding
{
args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709:tonemap={2}:peak={3}:desat={4}";
- if (string.Equals(options.TonemappingMode, "max", StringComparison.OrdinalIgnoreCase)
- || string.Equals(options.TonemappingMode, "rgb", StringComparison.OrdinalIgnoreCase))
+ var useLegacyTonemapModes = _mediaEncoder.EncoderVersion >= _minFFmpegOclCuTonemapMode
+ && _legacyTonemapModes.Contains(options.TonemappingMode, StringComparison.OrdinalIgnoreCase);
+
+ var useAdvancedTonemapModes = _mediaEncoder.EncoderVersion >= _minFFmpegAdvancedTonemapMode
+ && _advancedTonemapModes.Contains(options.TonemappingMode, StringComparison.OrdinalIgnoreCase);
+
+ if (useLegacyTonemapModes || useAdvancedTonemapModes)
{
- if (_mediaEncoder.EncoderVersion >= _minFFmpegOclCuTonemapMode)
- {
- args += ":tonemap_mode={5}";
- }
+ args += ":tonemap_mode={5}";
}
if (options.TonemappingParam != 0)
@@ -3492,15 +3374,7 @@ namespace MediaBrowser.Controller.MediaEncoding
algorithm = "clip";
}
- tonemapArg = ":tonemapping=" + algorithm;
-
- if (string.Equals(mode, "max", StringComparison.OrdinalIgnoreCase)
- || string.Equals(mode, "rgb", StringComparison.OrdinalIgnoreCase))
- {
- tonemapArg += ":tonemapping_mode=" + mode;
- }
-
- tonemapArg += ":peak_detect=0:color_primaries=bt709:color_trc=bt709:colorspace=bt709";
+ tonemapArg = ":tonemapping=" + algorithm + ":peak_detect=0:color_primaries=bt709:color_trc=bt709:colorspace=bt709";
if (string.Equals(range, "tv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(range, "pc", StringComparison.OrdinalIgnoreCase))
@@ -3517,6 +3391,18 @@ namespace MediaBrowser.Controller.MediaEncoding
tonemapArg);
}
+ public string GetVideoTransposeDirection(EncodingJobInfo state)
+ {
+ return (state.VideoStream?.Rotation ?? 0) switch
+ {
+ 90 => "cclock",
+ 180 => "reversal",
+ -90 => "clock",
+ -180 => "reversal",
+ _ => string.Empty
+ };
+ }
+
/// <summary>
/// Gets the parameter of software filter chain.
/// </summary>
@@ -3546,11 +3432,17 @@ namespace MediaBrowser.Controller.MediaEncoding
var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
var doDeintH2645 = doDeintH264 || doDeintHevc;
var doToneMap = IsSwTonemapAvailable(state, options);
+ var requireDoviReshaping = doToneMap && state.VideoStream.VideoRangeType == VideoRangeType.DOVI;
var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream;
+ var rotation = state.VideoStream?.Rotation ?? 0;
+ var swapWAndH = Math.Abs(rotation) == 90;
+ var swpInW = swapWAndH ? inH : inW;
+ var swpInH = swapWAndH ? inW : inH;
+
/* Make main filters for video stream */
var mainFilters = new List<string>();
@@ -3565,7 +3457,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var outFormat = isSwDecoder ? "yuv420p" : "nv12";
- var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
+ var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
if (isVaapiEncoder)
{
outFormat = "nv12";
@@ -3578,11 +3470,13 @@ namespace MediaBrowser.Controller.MediaEncoding
// sw scale
mainFilters.Add(swScaleFilter);
- // sw tonemap <= TODO: finish dovi tone mapping
-
+ // sw tonemap
if (doToneMap)
{
- var tonemapArgs = $"tonemapx=tonemap={options.TonemappingAlgorithm}:desat={options.TonemappingDesat}:peak={options.TonemappingPeak}:t=bt709:m=bt709:p=bt709:format={outFormat}";
+ // tonemapx requires yuv420p10 input for dovi reshaping, let ffmpeg convert the frame when necessary
+ var tonemapFormat = requireDoviReshaping ? "yuv420p" : outFormat;
+
+ var tonemapArgs = $"tonemapx=tonemap={options.TonemappingAlgorithm}:desat={options.TonemappingDesat}:peak={options.TonemappingPeak}:t=bt709:m=bt709:p=bt709:format={tonemapFormat}";
if (options.TonemappingParam != 0)
{
@@ -3614,7 +3508,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
@@ -3688,6 +3582,13 @@ namespace MediaBrowser.Controller.MediaEncoding
&& (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase));
+ var rotation = state.VideoStream?.Rotation ?? 0;
+ var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
+ var doCuTranspose = !string.IsNullOrEmpty(tranposeDir) && _mediaEncoder.SupportsFilter("transpose_cuda");
+ var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isNvDecoder && doCuTranspose));
+ var swpInW = swapWAndH ? inH : inW;
+ var swpInH = swapWAndH ? inW : inH;
+
/* Make main filters for video stream */
var mainFilters = new List<string>();
@@ -3704,10 +3605,10 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var outFormat = doCuTonemap ? "yuv420p10le" : "yuv420p";
- var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
+ var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
// sw scale
mainFilters.Add(swScaleFilter);
- mainFilters.Add("format=" + outFormat);
+ mainFilters.Add($"format={outFormat}");
// sw => hw
if (doCuTonemap)
@@ -3726,8 +3627,14 @@ namespace MediaBrowser.Controller.MediaEncoding
mainFilters.Add(deintFilter);
}
+ // hw transpose
+ if (doCuTranspose)
+ {
+ mainFilters.Add($"transpose_cuda=dir={tranposeDir}");
+ }
+
var outFormat = doCuTonemap ? string.Empty : "yuv420p";
- var hwScaleFilter = GetHwScaleFilter("cuda", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var hwScaleFilter = GetHwScaleFilter("scale", "cuda", outFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
// hw scale
mainFilters.Add(hwScaleFilter);
}
@@ -3777,7 +3684,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
subFilters.Add("format=yuva420p");
}
@@ -3787,7 +3694,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
// alphasrc=s=1280x720:r=10:start=0,format=yuva420p,subtitles,hwupload
- var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, subFramerate);
+ var alphaSrcFilter = GetAlphaSrcFilter(state, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH, subFramerate);
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
subFilters.Add(alphaSrcFilter);
subFilters.Add("format=yuva420p");
@@ -3802,7 +3709,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
@@ -3878,6 +3785,14 @@ namespace MediaBrowser.Controller.MediaEncoding
&& (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase));
+ var rotation = state.VideoStream?.Rotation ?? 0;
+ var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
+ var doOclTranspose = !string.IsNullOrEmpty(tranposeDir)
+ && _mediaEncoder.SupportsFilterWithOption(FilterOptionType.TransposeOpenclReversal);
+ var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isD3d11vaDecoder && doOclTranspose));
+ var swpInW = swapWAndH ? inH : inW;
+ var swpInH = swapWAndH ? inW : inH;
+
/* Make main filters for video stream */
var mainFilters = new List<string>();
@@ -3894,19 +3809,19 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var outFormat = doOclTonemap ? "yuv420p10le" : "yuv420p";
- var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
+ var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
// sw scale
mainFilters.Add(swScaleFilter);
- mainFilters.Add("format=" + outFormat);
+ mainFilters.Add($"format={outFormat}");
// keep video at memory except ocl tonemap,
// since the overhead caused by hwupload >>> using sw filter.
// sw => hw
if (doOclTonemap)
{
- mainFilters.Add("hwupload=derive_device=d3d11va:extra_hw_frames=16");
+ mainFilters.Add("hwupload=derive_device=d3d11va:extra_hw_frames=24");
mainFilters.Add("format=d3d11");
- mainFilters.Add("hwmap=derive_device=opencl");
+ mainFilters.Add("hwmap=derive_device=opencl:mode=read");
}
}
@@ -3914,12 +3829,18 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// INPUT d3d11 surface(vram)
// map from d3d11va to opencl via d3d11-opencl interop.
- mainFilters.Add("hwmap=derive_device=opencl");
+ mainFilters.Add("hwmap=derive_device=opencl:mode=read");
// hw deint <= TODO: finsh the 'yadif_opencl' filter
+ // hw transpose
+ if (doOclTranspose)
+ {
+ mainFilters.Add($"transpose_opencl=dir={tranposeDir}");
+ }
+
var outFormat = doOclTonemap ? string.Empty : "nv12";
- var hwScaleFilter = GetHwScaleFilter("opencl", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var hwScaleFilter = GetHwScaleFilter("scale", "opencl", outFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
// hw scale
mainFilters.Add(hwScaleFilter);
}
@@ -3964,7 +3885,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// OUTPUT d3d11(nv12) surface(vram)
// reverse-mapping via d3d11-opencl interop.
- mainFilters.Add("hwmap=derive_device=d3d11va:reverse=1");
+ mainFilters.Add("hwmap=derive_device=d3d11va:mode=write:reverse=1");
mainFilters.Add("format=d3d11");
}
@@ -3977,7 +3898,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
subFilters.Add("format=yuva420p");
}
@@ -3987,7 +3908,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
// alphasrc=s=1280x720:r=10:start=0,format=yuva420p,subtitles,hwupload
- var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, subFramerate);
+ var alphaSrcFilter = GetAlphaSrcFilter(state, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH, subFramerate);
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
subFilters.Add(alphaSrcFilter);
subFilters.Add("format=yuva420p");
@@ -3996,7 +3917,7 @@ namespace MediaBrowser.Controller.MediaEncoding
subFilters.Add("hwupload=derive_device=opencl");
overlayFilters.Add("overlay_opencl=eof_action=pass:repeatlast=0");
- overlayFilters.Add("hwmap=derive_device=d3d11va:reverse=1");
+ overlayFilters.Add("hwmap=derive_device=d3d11va:mode=write:reverse=1");
overlayFilters.Add("format=d3d11");
}
}
@@ -4004,7 +3925,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
@@ -4100,6 +4021,13 @@ namespace MediaBrowser.Controller.MediaEncoding
&& (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase));
+ var rotation = state.VideoStream?.Rotation ?? 0;
+ var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
+ var doVppTranspose = !string.IsNullOrEmpty(tranposeDir);
+ var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || ((isD3d11vaDecoder || isQsvDecoder) && doVppTranspose));
+ var swpInW = swapWAndH ? inH : inW;
+ var swpInH = swapWAndH ? inW : inH;
+
/* Make main filters for video stream */
var mainFilters = new List<string>();
@@ -4116,10 +4044,10 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12");
- var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
+ var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
// sw scale
mainFilters.Add(swScaleFilter);
- mainFilters.Add("format=" + outFormat);
+ mainFilters.Add($"format={outFormat}");
// keep video at memory except ocl tonemap,
// since the overhead caused by hwupload >>> using sw filter.
@@ -4131,8 +4059,15 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (isD3d11vaDecoder || isQsvDecoder)
{
- var outFormat = doOclTonemap ? string.Empty : "nv12";
- var hwScaleFilter = GetHwScaleFilter("qsv", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var outFormat = doOclTonemap ? (doVppTranspose ? "p010" : string.Empty) : "nv12";
+ var swapOutputWandH = doVppTranspose && swapWAndH;
+ var hwScalePrefix = doVppTranspose ? "vpp" : "scale";
+ var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, "qsv", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
+
+ if (!string.IsNullOrEmpty(hwScaleFilter) && doVppTranspose)
+ {
+ hwScaleFilter += $":transpose={tranposeDir}";
+ }
if (isD3d11vaDecoder)
{
@@ -4151,14 +4086,14 @@ namespace MediaBrowser.Controller.MediaEncoding
mainFilters.Add(deintFilter);
}
- // hw scale
+ // hw transpose & scale
mainFilters.Add(hwScaleFilter);
}
if (doOclTonemap && isHwDecoder)
{
// map from qsv to opencl via qsv(d3d11)-opencl interop.
- mainFilters.Add("hwmap=derive_device=opencl");
+ mainFilters.Add("hwmap=derive_device=opencl:mode=read");
}
// hw tonemap
@@ -4202,7 +4137,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// OUTPUT qsv(nv12) surface(vram)
// reverse-mapping via qsv(d3d11)-opencl interop.
- mainFilters.Add("hwmap=derive_device=qsv:reverse=1");
+ mainFilters.Add("hwmap=derive_device=qsv:mode=write:reverse=1");
mainFilters.Add("format=qsv");
}
@@ -4216,7 +4151,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (hasGraphicalSubs)
{
// overlay_qsv can handle overlay scaling, setup a smaller height to reduce transfer overhead
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, 1080);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, 1080);
subFilters.Add(subPreProcFilters);
subFilters.Add("format=bgra");
}
@@ -4226,7 +4161,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
// alphasrc=s=1280x720:r=10:start=0,format=bgra,subtitles,hwupload
- var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, 1080, subFramerate);
+ var alphaSrcFilter = GetAlphaSrcFilter(state, swpInW, swpInH, reqW, reqH, reqMaxW, 1080, subFramerate);
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
subFilters.Add(alphaSrcFilter);
subFilters.Add("format=bgra");
@@ -4237,9 +4172,9 @@ namespace MediaBrowser.Controller.MediaEncoding
// default to 64 otherwise it will fail on certain iGPU.
subFilters.Add("hwupload=derive_device=qsv:extra_hw_frames=64");
- var (overlayW, overlayH) = GetFixedOutputSize(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var (overlayW, overlayH) = GetFixedOutputSize(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
var overlaySize = (overlayW.HasValue && overlayH.HasValue)
- ? (":w=" + overlayW.Value + ":h=" + overlayH.Value)
+ ? $":w={overlayW.Value}:h={overlayH.Value}"
: string.Empty;
var overlayQsvFilter = string.Format(
CultureInfo.InvariantCulture,
@@ -4252,7 +4187,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
@@ -4297,6 +4232,13 @@ namespace MediaBrowser.Controller.MediaEncoding
&& (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase));
+ var rotation = state.VideoStream?.Rotation ?? 0;
+ var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
+ var doVppTranspose = !string.IsNullOrEmpty(tranposeDir);
+ var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || ((isVaapiDecoder || isQsvDecoder) && doVppTranspose));
+ var swpInW = swapWAndH ? inH : inW;
+ var swpInH = swapWAndH ? inW : inH;
+
/* Make main filters for video stream */
var mainFilters = new List<string>();
@@ -4313,10 +4255,10 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12");
- var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
+ var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
// sw scale
mainFilters.Add(swScaleFilter);
- mainFilters.Add("format=" + outFormat);
+ mainFilters.Add($"format={outFormat}");
// keep video at memory except ocl tonemap,
// since the overhead caused by hwupload >>> using sw filter.
@@ -4328,24 +4270,39 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (isVaapiDecoder || isQsvDecoder)
{
+ var hwFilterSuffix = isVaapiDecoder ? "vaapi" : "qsv";
+
// INPUT vaapi/qsv surface(vram)
// hw deint
if (doDeintH2645)
{
- var deintFilter = GetHwDeinterlaceFilter(state, options, isVaapiDecoder ? "vaapi" : "qsv");
+ var deintFilter = GetHwDeinterlaceFilter(state, options, hwFilterSuffix);
mainFilters.Add(deintFilter);
}
- var outFormat = doTonemap ? string.Empty : "nv12";
- var hwScaleFilter = GetHwScaleFilter(isVaapiDecoder ? "vaapi" : "qsv", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ // hw transpose(vaapi vpp)
+ if (isVaapiDecoder && doVppTranspose)
+ {
+ mainFilters.Add($"transpose_vaapi=dir={tranposeDir}");
+ }
- // allocate extra pool sizes for vaapi vpp
+ var outFormat = doOclTonemap ? ((isQsvDecoder && doVppTranspose) ? "p010" : string.Empty) : "nv12";
+ var swapOutputWandH = isQsvDecoder && doVppTranspose && swapWAndH;
+ var hwScalePrefix = (isQsvDecoder && doVppTranspose) ? "vpp" : "scale";
+ var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, hwFilterSuffix, outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
+
+ if (!string.IsNullOrEmpty(hwScaleFilter) && isQsvDecoder && doVppTranspose)
+ {
+ hwScaleFilter += $":transpose={tranposeDir}";
+ }
+
+ // allocate extra pool sizes for vaapi vpp scale
if (!string.IsNullOrEmpty(hwScaleFilter) && isVaapiDecoder)
{
hwScaleFilter += ":extra_hw_frames=24";
}
- // hw scale
+ // hw transpose(qsv vpp) & scale
mainFilters.Add(hwScaleFilter);
}
@@ -4373,7 +4330,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (doOclTonemap && isHwDecoder)
{
// map from qsv to opencl via qsv(vaapi)-opencl interop.
- mainFilters.Add("hwmap=derive_device=opencl");
+ mainFilters.Add("hwmap=derive_device=opencl:mode=read");
}
// ocl tonemap
@@ -4420,7 +4377,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// OUTPUT qsv(nv12) surface(vram)
// reverse-mapping via qsv(vaapi)-opencl interop.
// add extra pool size to avoid the 'cannot allocate memory' error on hevc_qsv.
- mainFilters.Add("hwmap=derive_device=qsv:reverse=1:extra_hw_frames=16");
+ mainFilters.Add("hwmap=derive_device=qsv:mode=write:reverse=1:extra_hw_frames=16");
mainFilters.Add("format=qsv");
}
else if (isVaapiDecoder)
@@ -4440,7 +4397,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (hasGraphicalSubs)
{
// overlay_qsv can handle overlay scaling, setup a smaller height to reduce transfer overhead
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, 1080);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, 1080);
subFilters.Add(subPreProcFilters);
subFilters.Add("format=bgra");
}
@@ -4449,7 +4406,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var framerate = state.VideoStream?.RealFrameRate;
var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
- var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, 1080, subFramerate);
+ var alphaSrcFilter = GetAlphaSrcFilter(state, swpInW, swpInH, reqW, reqH, reqMaxW, 1080, subFramerate);
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
subFilters.Add(alphaSrcFilter);
subFilters.Add("format=bgra");
@@ -4460,9 +4417,9 @@ namespace MediaBrowser.Controller.MediaEncoding
// default to 64 otherwise it will fail on certain iGPU.
subFilters.Add("hwupload=derive_device=qsv:extra_hw_frames=64");
- var (overlayW, overlayH) = GetFixedOutputSize(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var (overlayW, overlayH) = GetFixedOutputSize(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
var overlaySize = (overlayW.HasValue && overlayH.HasValue)
- ? (":w=" + overlayW.Value + ":h=" + overlayH.Value)
+ ? $":w={overlayW.Value}:h={overlayH.Value}"
: string.Empty;
var overlayQsvFilter = string.Format(
CultureInfo.InvariantCulture,
@@ -4475,7 +4432,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
@@ -4586,6 +4543,13 @@ namespace MediaBrowser.Controller.MediaEncoding
&& (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase));
+ var rotation = state.VideoStream?.Rotation ?? 0;
+ var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
+ var doVaVppTranspose = !string.IsNullOrEmpty(tranposeDir);
+ var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isVaapiDecoder && doVaVppTranspose));
+ var swpInW = swapWAndH ? inH : inW;
+ var swpInH = swapWAndH ? inW : inH;
+
/* Make main filters for video stream */
var mainFilters = new List<string>();
@@ -4602,10 +4566,10 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var outFormat = doOclTonemap ? "yuv420p10le" : "nv12";
- var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
+ var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
// sw scale
mainFilters.Add(swScaleFilter);
- mainFilters.Add("format=" + outFormat);
+ mainFilters.Add($"format={outFormat}");
// keep video at memory except ocl tonemap,
// since the overhead caused by hwupload >>> using sw filter.
@@ -4625,8 +4589,14 @@ namespace MediaBrowser.Controller.MediaEncoding
mainFilters.Add(deintFilter);
}
+ // hw transpose
+ if (doVaVppTranspose)
+ {
+ mainFilters.Add($"transpose_vaapi=dir={tranposeDir}");
+ }
+
var outFormat = doTonemap ? string.Empty : "nv12";
- var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var hwScaleFilter = GetHwScaleFilter("scale", "vaapi", outFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
// allocate extra pool sizes for vaapi vpp
if (!string.IsNullOrEmpty(hwScaleFilter))
@@ -4648,7 +4618,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (doOclTonemap && isVaapiDecoder)
{
// map from vaapi to opencl via vaapi-opencl interop(Intel only).
- mainFilters.Add("hwmap=derive_device=opencl");
+ mainFilters.Add("hwmap=derive_device=opencl:mode=read");
}
// ocl tonemap
@@ -4662,7 +4632,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// OUTPUT vaapi(nv12) surface(vram)
// reverse-mapping via vaapi-opencl interop.
- mainFilters.Add("hwmap=derive_device=vaapi:reverse=1");
+ mainFilters.Add("hwmap=derive_device=vaapi:mode=write:reverse=1");
mainFilters.Add("format=vaapi");
}
@@ -4713,7 +4683,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (hasGraphicalSubs)
{
// overlay_vaapi can handle overlay scaling, setup a smaller height to reduce transfer overhead
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, 1080);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, 1080);
subFilters.Add(subPreProcFilters);
subFilters.Add("format=bgra");
}
@@ -4722,7 +4692,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var framerate = state.VideoStream?.RealFrameRate;
var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
- var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, 1080, subFramerate);
+ var alphaSrcFilter = GetAlphaSrcFilter(state, swpInW, swpInH, reqW, reqH, reqMaxW, 1080, subFramerate);
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
subFilters.Add(alphaSrcFilter);
subFilters.Add("format=bgra");
@@ -4731,9 +4701,9 @@ namespace MediaBrowser.Controller.MediaEncoding
subFilters.Add("hwupload=derive_device=vaapi");
- var (overlayW, overlayH) = GetFixedOutputSize(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var (overlayW, overlayH) = GetFixedOutputSize(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
var overlaySize = (overlayW.HasValue && overlayH.HasValue)
- ? (":w=" + overlayW.Value + ":h=" + overlayH.Value)
+ ? $":w={overlayW.Value}:h={overlayH.Value}"
: string.Empty;
var overlayVaapiFilter = string.Format(
CultureInfo.InvariantCulture,
@@ -4746,7 +4716,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
@@ -4791,6 +4761,13 @@ namespace MediaBrowser.Controller.MediaEncoding
&& (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase));
+ var rotation = state.VideoStream?.Rotation ?? 0;
+ var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
+ var doVkTranspose = isVaapiDecoder && !string.IsNullOrEmpty(tranposeDir);
+ var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isVaapiDecoder && doVkTranspose));
+ var swpInW = swapWAndH ? inH : inW;
+ var swpInH = swapWAndH ? inW : inH;
+
/* Make main filters for video stream */
var mainFilters = new List<string>();
@@ -4815,7 +4792,7 @@ namespace MediaBrowser.Controller.MediaEncoding
else
{
// sw scale
- var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
+ var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
mainFilters.Add(swScaleFilter);
mainFilters.Add("format=nv12");
}
@@ -4823,11 +4800,37 @@ namespace MediaBrowser.Controller.MediaEncoding
else if (isVaapiDecoder)
{
// INPUT vaapi surface(vram)
- if (doVkTonemap || hasSubs)
+ if (doVkTranspose || doVkTonemap || hasSubs)
{
// map from vaapi to vulkan/drm via interop (Polaris/gfx8+).
- mainFilters.Add("hwmap=derive_device=vulkan");
- mainFilters.Add("format=vulkan");
+ if (_mediaEncoder.EncoderVersion >= _minFFmpegAlteredVaVkInterop)
+ {
+ if (doVkTranspose || !_mediaEncoder.IsVaapiDeviceSupportVulkanDrmModifier)
+ {
+ // disable the indirect va-drm-vk mapping since it's no longer reliable.
+ mainFilters.Add("hwmap=derive_device=drm");
+ mainFilters.Add("format=drm_prime");
+ mainFilters.Add("hwmap=derive_device=vulkan");
+ mainFilters.Add("format=vulkan");
+
+ // workaround for libplacebo using the imported vulkan frame on gfx8.
+ if (!_mediaEncoder.IsVaapiDeviceSupportVulkanDrmModifier)
+ {
+ mainFilters.Add("scale_vulkan");
+ }
+ }
+ else if (doVkTonemap || hasSubs)
+ {
+ // non ad-hoc libplacebo also accepts drm_prime direct input.
+ mainFilters.Add("hwmap=derive_device=drm");
+ mainFilters.Add("format=drm_prime");
+ }
+ }
+ else // legacy va-vk mapping that works only in jellyfin-ffmpeg6
+ {
+ mainFilters.Add("hwmap=derive_device=vulkan");
+ mainFilters.Add("format=vulkan");
+ }
}
else
{
@@ -4839,16 +4842,30 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// hw scale
- var hwScaleFilter = GetHwScaleFilter("vaapi", "nv12", inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var hwScaleFilter = GetHwScaleFilter("scale", "vaapi", "nv12", false, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
mainFilters.Add(hwScaleFilter);
}
}
+ // vk transpose
+ if (doVkTranspose)
+ {
+ if (string.Equals(tranposeDir, "reversal", StringComparison.OrdinalIgnoreCase))
+ {
+ mainFilters.Add("flip_vulkan");
+ }
+ else
+ {
+ mainFilters.Add($"transpose_vulkan=dir={tranposeDir}");
+ }
+ }
+
// vk libplacebo
if (doVkTonemap || hasSubs)
{
- var libplaceboFilter = GetLibplaceboFilter(options, "bgra", doVkTonemap, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var libplaceboFilter = GetLibplaceboFilter(options, "bgra", doVkTonemap, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
mainFilters.Add(libplaceboFilter);
+ mainFilters.Add("format=vulkan");
}
if (doVkTonemap && !hasSubs)
@@ -4891,7 +4908,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
subFilters.Add("format=bgra");
}
@@ -4900,7 +4917,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var framerate = state.VideoStream?.RealFrameRate;
var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
- var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, subFramerate);
+ var alphaSrcFilter = GetAlphaSrcFilter(state, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH, subFramerate);
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
subFilters.Add(alphaSrcFilter);
subFilters.Add("format=bgra");
@@ -4972,6 +4989,11 @@ namespace MediaBrowser.Controller.MediaEncoding
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream;
+ var rotation = state.VideoStream?.Rotation ?? 0;
+ var swapWAndH = Math.Abs(rotation) == 90 && isSwDecoder;
+ var swpInW = swapWAndH ? inH : inW;
+ var swpInH = swapWAndH ? inW : inH;
+
/* Make main filters for video stream */
var mainFilters = new List<string>();
@@ -4989,7 +5011,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
outFormat = doOclTonemap ? "yuv420p10le" : "nv12";
- var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
+ var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
// sw scale
mainFilters.Add(swScaleFilter);
mainFilters.Add("format=" + outFormat);
@@ -5013,7 +5035,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
outFormat = doOclTonemap ? string.Empty : "nv12";
- var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var hwScaleFilter = GetHwScaleFilter("scale", "vaapi", outFormat, false, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
// allocate extra pool sizes for vaapi vpp
if (!string.IsNullOrEmpty(hwScaleFilter))
@@ -5109,7 +5131,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
@@ -5140,13 +5162,15 @@ namespace MediaBrowser.Controller.MediaEncoding
return (null, null, null);
}
+ // ReSharper disable once InconsistentNaming
var isMacOS = OperatingSystem.IsMacOS();
var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty;
+ var isVtDecoder = vidDecoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
var isVtFullSupported = isMacOS && IsVideoToolboxFullSupported();
// legacy videotoolbox pipeline (disable hw filters)
- if (!isVtEncoder
+ if (!(isVtEncoder || isVtDecoder)
|| !isVtFullSupported
|| !_mediaEncoder.SupportsFilter("alphasrc"))
{
@@ -5163,6 +5187,9 @@ namespace MediaBrowser.Controller.MediaEncoding
string vidDecoder,
string vidEncoder)
{
+ var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
+ var isVtDecoder = vidDecoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
+
var inW = state.VideoStream?.Width;
var inH = state.VideoStream?.Height;
var reqW = state.BaseRequest.Width;
@@ -5171,13 +5198,19 @@ namespace MediaBrowser.Controller.MediaEncoding
var reqMaxH = state.BaseRequest.MaxHeight;
var threeDFormat = state.MediaSource.Video3DFormat;
- var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
-
var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
var doDeintH2645 = doDeintH264 || doDeintHevc;
var doVtTonemap = IsVideoToolboxTonemapAvailable(state, options);
var doMetalTonemap = !doVtTonemap && IsHwTonemapAvailable(state, options);
+ var usingHwSurface = isVtDecoder && (_mediaEncoder.EncoderVersion >= _minFFmpegWorkingVtHwSurface);
+
+ var rotation = state.VideoStream?.Rotation ?? 0;
+ var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
+ var doVtTranspose = !string.IsNullOrEmpty(tranposeDir) && _mediaEncoder.SupportsFilter("transpose_vt");
+ var swapWAndH = Math.Abs(rotation) == 90 && doVtTranspose;
+ var swpInW = swapWAndH ? inH : inW;
+ var swpInH = swapWAndH ? inW : inH;
var scaleFormat = string.Empty;
// Use P010 for Metal tone mapping, otherwise force an 8bit output.
@@ -5196,7 +5229,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
- var hwScaleFilter = GetHwScaleFilter("vt", scaleFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var hwScaleFilter = GetHwScaleFilter("scale", "vt", scaleFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
@@ -5205,12 +5238,6 @@ namespace MediaBrowser.Controller.MediaEncoding
&& (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase));
- if (!isVtEncoder)
- {
- // should not happen.
- return (null, null, null);
- }
-
/* Make main filters for video stream */
var mainFilters = new List<string>();
@@ -5221,6 +5248,12 @@ namespace MediaBrowser.Controller.MediaEncoding
mainFilters.Add(deintFilter);
}
+ // hw transpose
+ if (doVtTranspose)
+ {
+ mainFilters.Add($"transpose_vt=dir={tranposeDir}");
+ }
+
if (doVtTonemap)
{
const string VtTonemapArgs = "color_matrix=bt709:color_primaries=bt709:color_transfer=bt709";
@@ -5249,7 +5282,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
subFilters.Add("format=bgra");
}
@@ -5258,30 +5291,44 @@ namespace MediaBrowser.Controller.MediaEncoding
var framerate = state.VideoStream?.RealFrameRate;
var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
- var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, subFramerate);
+ var alphaSrcFilter = GetAlphaSrcFilter(state, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH, subFramerate);
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
subFilters.Add(alphaSrcFilter);
subFilters.Add("format=bgra");
subFilters.Add(subTextSubtitlesFilter);
}
- subFilters.Add("hwupload=derive_device=videotoolbox");
+ subFilters.Add("hwupload");
overlayFilters.Add("overlay_videotoolbox=eof_action=pass:repeatlast=0");
}
+ if (usingHwSurface)
+ {
+ if (!isVtEncoder)
+ {
+ mainFilters.Add("hwdownload");
+ mainFilters.Add("format=nv12");
+ }
+
+ return (mainFilters, subFilters, overlayFilters);
+ }
+
+ // For old jellyfin-ffmpeg that has broken hwsurface, add a hwupload
var needFiltering = mainFilters.Any(f => !string.IsNullOrEmpty(f)) ||
subFilters.Any(f => !string.IsNullOrEmpty(f)) ||
overlayFilters.Any(f => !string.IsNullOrEmpty(f));
-
- // This is a workaround for ffmpeg's hwupload implementation
- // For VideoToolbox encoders, a hwupload without a valid filter actually consuming its frame
- // will cause the encoder to produce incorrect frames.
if (needFiltering)
{
// INPUT videotoolbox/memory surface(vram/uma)
// this will pass-through automatically if in/out format matches.
+ mainFilters.Insert(0, "hwupload");
mainFilters.Insert(0, "format=nv12|p010le|videotoolbox_vld");
- mainFilters.Insert(0, "hwupload=derive_device=videotoolbox");
+
+ if (!isVtEncoder)
+ {
+ mainFilters.Add("hwdownload");
+ mainFilters.Add("format=nv12");
+ }
}
return (mainFilters, subFilters, overlayFilters);
@@ -5358,6 +5405,13 @@ namespace MediaBrowser.Controller.MediaEncoding
&& (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase));
+ var rotation = state.VideoStream?.Rotation ?? 0;
+ var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
+ var doRkVppTranspose = !string.IsNullOrEmpty(tranposeDir);
+ var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isRkmppDecoder && doRkVppTranspose));
+ var swpInW = swapWAndH ? inH : inW;
+ var swpInH = swapWAndH ? inW : inH;
+
/* Make main filters for video stream */
var mainFilters = new List<string>();
@@ -5374,7 +5428,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12");
- var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
+ var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
if (!string.IsNullOrEmpty(swScaleFilter))
{
swScaleFilter += ":flags=fast_bilinear";
@@ -5382,7 +5436,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// sw scale
mainFilters.Add(swScaleFilter);
- mainFilters.Add("format=" + outFormat);
+ mainFilters.Add($"format={outFormat}");
// keep video at memory except ocl tonemap,
// since the overhead caused by hwupload >>> using sw filter.
@@ -5397,21 +5451,29 @@ namespace MediaBrowser.Controller.MediaEncoding
// INPUT rkmpp/drm surface(gem/dma-heap)
var isFullAfbcPipeline = isDrmInDrmOut && !doOclTonemap;
+ var swapOutputWandH = doRkVppTranspose && swapWAndH;
var outFormat = doOclTonemap ? "p010" : "nv12";
- var hwScaleFilter = GetHwScaleFilter("rkrga", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
- var hwScaleFilter2 = GetHwScaleFilter("rkrga", string.Empty, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var hwScalePrefix = doRkVppTranspose ? "vpp" : "scale";
+ var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, "rkrga", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
+ var hwScaleFilter2 = GetHwScaleFilter(hwScalePrefix, "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
if (!hasSubs
+ || doRkVppTranspose
|| !isFullAfbcPipeline
|| !string.IsNullOrEmpty(hwScaleFilter2))
{
+ if (!string.IsNullOrEmpty(hwScaleFilter) && doRkVppTranspose)
+ {
+ hwScaleFilter += $":transpose={tranposeDir}";
+ }
+
// try enabling AFBC to save DDR bandwidth
if (!string.IsNullOrEmpty(hwScaleFilter) && isFullAfbcPipeline)
{
hwScaleFilter += ":afbc=1";
}
- // hw scale
+ // hw transpose & scale
mainFilters.Add(hwScaleFilter);
}
}
@@ -5482,7 +5544,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
subFilters.Add("format=bgra");
}
@@ -5492,7 +5554,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
// alphasrc=s=1280x720:r=10:start=0,format=bgra,subtitles,hwupload
- var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, subFramerate);
+ var alphaSrcFilter = GetAlphaSrcFilter(state, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH, subFramerate);
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
subFilters.Add(alphaSrcFilter);
subFilters.Add("format=bgra");
@@ -5509,7 +5571,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
@@ -5924,6 +5986,11 @@ namespace MediaBrowser.Controller.MediaEncoding
// Disable the extra internal copy in nvdec. We already handle it in filter chain.
var nvdecNoInternalCopy = ffmpegVersion >= _minFFmpegHwaUnsafeOutput;
+ // Strip the display rotation side data from the transposed fmp4 output stream.
+ var stripRotationData = (state.VideoStream?.Rotation ?? 0) != 0
+ && ffmpegVersion >= _minFFmpegDisplayRotationOption;
+ var stripRotationDataArgs = stripRotationData ? " -display_rotation 0" : string.Empty;
+
if (bitDepth == 10 && isCodecAvailable)
{
if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
@@ -5948,13 +6015,13 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (isVaapiSupported && isCodecAvailable)
{
- return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty)
+ return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi -noautorotate" + stripRotationDataArgs : string.Empty)
+ (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
}
if (isD3d11Supported && isCodecAvailable)
{
- return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty)
+ return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11 -noautorotate" + stripRotationDataArgs : string.Empty)
+ (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + " -threads 2" + (isAv1 ? " -c:v av1" : string.Empty);
}
}
@@ -5962,7 +6029,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (isQsvSupported && isCodecAvailable)
{
- return " -hwaccel qsv" + (outputHwSurface ? " -hwaccel_output_format qsv" : string.Empty);
+ return " -hwaccel qsv" + (outputHwSurface ? " -hwaccel_output_format qsv -noautorotate" + stripRotationDataArgs : string.Empty);
}
}
}
@@ -5975,12 +6042,12 @@ namespace MediaBrowser.Controller.MediaEncoding
if (options.EnableEnhancedNvdecDecoder)
{
// set -threads 1 to nvdec decoder explicitly since it doesn't implement threading support.
- return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty)
+ return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda -noautorotate" + stripRotationDataArgs : string.Empty)
+ (nvdecNoInternalCopy ? " -hwaccel_flags +unsafe_output" : string.Empty) + " -threads 1" + (isAv1 ? " -c:v av1" : string.Empty);
}
// cuvid decoder doesn't have threading issue.
- return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty);
+ return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda -noautorotate" + stripRotationDataArgs : string.Empty);
}
}
@@ -5989,7 +6056,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (isD3d11Supported && isCodecAvailable)
{
- return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty)
+ return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11 -noautorotate" + stripRotationDataArgs : string.Empty)
+ (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
}
}
@@ -5999,7 +6066,7 @@ namespace MediaBrowser.Controller.MediaEncoding
&& isVaapiSupported
&& isCodecAvailable)
{
- return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty)
+ return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi -noautorotate" + stripRotationDataArgs : string.Empty)
+ (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
}
@@ -6008,7 +6075,7 @@ namespace MediaBrowser.Controller.MediaEncoding
&& isVideotoolboxSupported
&& isCodecAvailable)
{
- return " -hwaccel videotoolbox" + (outputHwSurface ? " -hwaccel_output_format videotoolbox_vld" : string.Empty);
+ return " -hwaccel videotoolbox" + (outputHwSurface ? " -hwaccel_output_format videotoolbox_vld" : string.Empty) + " -noautorotate" + stripRotationDataArgs;
}
// Rockchip rkmpp
@@ -6016,7 +6083,7 @@ namespace MediaBrowser.Controller.MediaEncoding
&& isRkmppSupported
&& isCodecAvailable)
{
- return " -hwaccel rkmpp" + (outputHwSurface ? " -hwaccel_output_format drm_prime" : string.Empty);
+ return " -hwaccel rkmpp" + (outputHwSurface ? " -hwaccel_output_format drm_prime -noautorotate" + stripRotationDataArgs : string.Empty);
}
return null;
@@ -6289,36 +6356,34 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
var is8_10bitSwFormatsVt = is8bitSwFormatsVt || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
- // VideoToolbox's Hardware surface in ffmpeg is not only slower than hwupload, but also breaks HDR in many cases.
- // For example: https://trac.ffmpeg.org/ticket/10884
- // Disable it for now.
- const bool UseHwSurface = false;
+ // The related patches make videotoolbox hardware surface working is only available in jellyfin-ffmpeg 7.0.1 at the moment.
+ bool useHwSurface = (_mediaEncoder.EncoderVersion >= _minFFmpegWorkingVtHwSurface) && IsVideoToolboxFullSupported();
if (is8bitSwFormatsVt)
{
- if (string.Equals("avc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
- || string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
- {
- return GetHwaccelType(state, options, "h264", bitDepth, UseHwSurface);
- }
-
if (string.Equals("vp8", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
- return GetHwaccelType(state, options, "vp8", bitDepth, UseHwSurface);
+ return GetHwaccelType(state, options, "vp8", bitDepth, useHwSurface);
}
}
if (is8_10bitSwFormatsVt)
{
+ if (string.Equals("avc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
+ {
+ return GetHwaccelType(state, options, "h264", bitDepth, useHwSurface);
+ }
+
if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
|| string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
- return GetHwaccelType(state, options, "hevc", bitDepth, UseHwSurface);
+ return GetHwaccelType(state, options, "hevc", bitDepth, useHwSurface);
}
if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
- return GetHwaccelType(state, options, "vp9", bitDepth, UseHwSurface);
+ return GetHwaccelType(state, options, "vp9", bitDepth, useHwSurface);
}
}
@@ -6431,24 +6496,15 @@ namespace MediaBrowser.Controller.MediaEncoding
#nullable enable
public static int GetNumberOfThreads(EncodingJobInfo? state, EncodingOptions encodingOptions, string? outputVideoCodec)
{
- // VP8 and VP9 encoders must have their thread counts set.
- bool mustSetThreadCount = string.Equals(outputVideoCodec, "libvpx", StringComparison.OrdinalIgnoreCase)
- || string.Equals(outputVideoCodec, "libvpx-vp9", StringComparison.OrdinalIgnoreCase);
-
var threads = state?.BaseRequest.CpuCoreLimit ?? encodingOptions.EncodingThreadCount;
if (threads <= 0)
{
// Automatically set thread count
- return mustSetThreadCount ? Math.Max(Environment.ProcessorCount - 1, 1) : 0;
- }
-
- if (threads >= Environment.ProcessorCount)
- {
- return Environment.ProcessorCount;
+ return 0;
}
- return threads;
+ return Math.Min(threads, Environment.ProcessorCount);
}
#nullable disable
diff --git a/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs
index b1d319d21..a2b6e1d73 100644
--- a/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs
+++ b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs
@@ -33,6 +33,11 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// The overlay_vulkan_framesync.
/// </summary>
- OverlayVulkanFrameSync = 5
+ OverlayVulkanFrameSync = 5,
+
+ /// <summary>
+ /// The transpose_opencl_reversal.
+ /// </summary>
+ TransposeOpenclReversal = 6
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index 038c6c7f6..c767b4a51 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -66,6 +66,12 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Gets a value indicating whether the configured Vaapi device supports vulkan drm format modifier.
/// </summary>
+ /// <value><c>true</c> if the Vaapi device supports vulkan drm format modifier, <c>false</c> otherwise.</value>
+ bool IsVaapiDeviceSupportVulkanDrmModifier { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether the configured Vaapi device supports vulkan drm interop via dma-buf.
+ /// </summary>
/// <value><c>true</c> if the Vaapi device supports vulkan drm interop, <c>false</c> otherwise.</value>
bool IsVaapiDeviceSupportVulkanDrmInterop { get; }
@@ -223,14 +229,8 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Sets the path to find FFmpeg.
/// </summary>
- void SetFFmpegPath();
-
- /// <summary>
- /// Updates the encoder path.
- /// </summary>
- /// <param name="path">The path.</param>
- /// <param name="pathType">The type of path.</param>
- void UpdateEncoderPath(string path, string pathType);
+ /// <returns>bool indicates whether a valid ffmpeg is found.</returns>
+ bool SetFFmpegPath();
/// <summary>
/// Gets the primary playlist of .vob files.
diff --git a/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs b/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs
new file mode 100644
index 000000000..4fcf084e1
--- /dev/null
+++ b/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.MediaSegments;
+
+namespace MediaBrowser.Controller;
+
+/// <summary>
+/// Defines methods for interacting with media segments.
+/// </summary>
+public interface IMediaSegmentManager
+{
+ /// <summary>
+ /// Returns if this item supports media segments.
+ /// </summary>
+ /// <param name="baseItem">The base Item to check.</param>
+ /// <returns>True if supported otherwise false.</returns>
+ bool IsTypeSupported(BaseItem baseItem);
+
+ /// <summary>
+ /// Creates a new Media Segment associated with an Item.
+ /// </summary>
+ /// <param name="mediaSegment">The segment to create.</param>
+ /// <param name="segmentProviderId">The id of the Provider who created this segment.</param>
+ /// <returns>The created Segment entity.</returns>
+ Task<MediaSegmentDto> CreateSegmentAsync(MediaSegmentDto mediaSegment, string segmentProviderId);
+
+ /// <summary>
+ /// Deletes a single media segment.
+ /// </summary>
+ /// <param name="segmentId">The <see cref="MediaSegment.Id"/> to delete.</param>
+ /// <returns>a task.</returns>
+ Task DeleteSegmentAsync(Guid segmentId);
+
+ /// <summary>
+ /// Obtains all segments accociated with the itemId.
+ /// </summary>
+ /// <param name="itemId">The id of the <see cref="BaseItem"/>.</param>
+ /// <param name="typeFilter">filteres all media segments of the given type to be included. If null all types are included.</param>
+ /// <returns>An enumerator of <see cref="MediaSegmentDto"/>'s.</returns>
+ Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter);
+
+ /// <summary>
+ /// Gets information about any media segments stored for the given itemId.
+ /// </summary>
+ /// <param name="itemId">The id of the <see cref="BaseItem"/>.</param>
+ /// <returns>True if there are any segments stored for the item, otherwise false.</returns>
+ /// TODO: this should be async but as the only caller BaseItem.GetVersionInfo isn't async, this is also not. Venson.
+ bool HasSegments(Guid itemId);
+}
diff --git a/MediaBrowser.Controller/Providers/SeasonInfo.cs b/MediaBrowser.Controller/Providers/SeasonInfo.cs
index 1edceb0e4..3af5ec2a3 100644
--- a/MediaBrowser.Controller/Providers/SeasonInfo.cs
+++ b/MediaBrowser.Controller/Providers/SeasonInfo.cs
@@ -1,4 +1,5 @@
#pragma warning disable CA2227, CS1591
+#nullable disable
using System;
using System.Collections.Generic;
@@ -13,5 +14,7 @@ namespace MediaBrowser.Controller.Providers
}
public Dictionary<string, string> SeriesProviderIds { get; set; }
+
+ public string SeriesDisplayOrder { get; set; }
}
}
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index 914990558..7e307286a 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -89,7 +89,8 @@ namespace MediaBrowser.MediaEncoding.Attachments
string outputPath,
CancellationToken cancellationToken)
{
- var shouldExtractOneByOne = mediaSource.MediaAttachments.Any(a => a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase));
+ var shouldExtractOneByOne = mediaSource.MediaAttachments.Any(a => !string.IsNullOrEmpty(a.FileName)
+ && (a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase)));
if (shouldExtractOneByOne)
{
var attachmentIndexes = mediaSource.MediaAttachments.Select(a => a.Index);
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index a865b0e4c..6c43be3ab 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -62,10 +62,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
"libx264",
"libx265",
"libsvtav1",
- "mpeg4",
- "msmpeg4",
- "libvpx",
- "libvpx-vp9",
"aac",
"aac_at",
"libfdk_aac",
@@ -116,25 +112,31 @@ namespace MediaBrowser.MediaEncoding.Encoder
"yadif_cuda",
"tonemap_cuda",
"overlay_cuda",
+ "transpose_cuda",
"hwupload_cuda",
// opencl
"scale_opencl",
"tonemap_opencl",
"overlay_opencl",
+ "transpose_opencl",
// vaapi
"scale_vaapi",
"deinterlace_vaapi",
"tonemap_vaapi",
"procamp_vaapi",
"overlay_vaapi",
+ "transpose_vaapi",
"hwupload_vaapi",
// vulkan
"libplacebo",
"scale_vulkan",
"overlay_vulkan",
+ "transpose_vulkan",
+ "flip_vulkan",
// videotoolbox
"yadif_videotoolbox",
"scale_vt",
+ "transpose_vt",
"overlay_videotoolbox",
"tonemap_videotoolbox",
// rkrga
@@ -150,7 +152,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
{ 2, new string[] { "tonemap_opencl", "bt2390" } },
{ 3, new string[] { "overlay_opencl", "Action to take when encountering EOF from secondary input" } },
{ 4, new string[] { "overlay_vaapi", "Action to take when encountering EOF from secondary input" } },
- { 5, new string[] { "overlay_vulkan", "Action to take when encountering EOF from secondary input" } }
+ { 5, new string[] { "overlay_vulkan", "Action to take when encountering EOF from secondary input" } },
+ { 6, new string[] { "transpose_opencl", "rotate by half-turn" } }
};
// These are the library versions that corresponds to our minimum ffmpeg version 4.4 according to the version table below
@@ -171,6 +174,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
private readonly string _encoderPath;
+ private readonly Version _minFFmpegMultiThreadedCli = new Version(7, 0);
+
public EncoderValidator(ILogger logger, string encoderPath)
{
_logger = logger;
@@ -480,7 +485,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
return false;
}
- public bool CheckSupportedRuntimeKey(string keyDesc)
+ public bool CheckSupportedRuntimeKey(string keyDesc, Version? ffmpegVersion)
{
if (string.IsNullOrEmpty(keyDesc))
{
@@ -490,7 +495,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
string output;
try
{
- output = GetProcessOutput(_encoderPath, "-hide_banner -f lavfi -i nullsrc=s=1x1:d=500 -f null -", true, "?");
+ // With multi-threaded cli support, FFmpeg 7 is less sensitive to keyboard input
+ var duration = ffmpegVersion >= _minFFmpegMultiThreadedCli ? 10000 : 1000;
+ output = GetProcessOutput(_encoderPath, $"-hide_banner -f lavfi -i nullsrc=s=1x1:d={duration} -f null -", true, "?");
}
catch (Exception ex)
{
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
index c5f500e76..2daeac734 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
@@ -42,7 +42,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
// If there's more than one we'll need to use the concat command
if (inputFiles.Count > 1)
{
- var files = string.Join("|", inputFiles.Select(NormalizePath));
+ var files = string.Join('|', inputFiles.Select(NormalizePath));
return string.Format(CultureInfo.InvariantCulture, "concat:\"{0}\"", files);
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 5cfead502..764230feb 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -12,6 +12,7 @@ using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters;
@@ -79,8 +80,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
private bool _isVaapiDeviceAmd = false;
private bool _isVaapiDeviceInteliHD = false;
private bool _isVaapiDeviceInteli965 = false;
+ private bool _isVaapiDeviceSupportVulkanDrmModifier = false;
private bool _isVaapiDeviceSupportVulkanDrmInterop = false;
+ private static string[] _vulkanImageDrmFmtModifierExts =
+ {
+ "VK_EXT_image_drm_format_modifier",
+ };
+
private static string[] _vulkanExternalMemoryDmaBufExts =
{
"VK_KHR_external_memory_fd",
@@ -141,34 +148,50 @@ namespace MediaBrowser.MediaEncoding.Encoder
public bool IsVaapiDeviceInteli965 => _isVaapiDeviceInteli965;
/// <inheritdoc />
+ public bool IsVaapiDeviceSupportVulkanDrmModifier => _isVaapiDeviceSupportVulkanDrmModifier;
+
+ /// <inheritdoc />
public bool IsVaapiDeviceSupportVulkanDrmInterop => _isVaapiDeviceSupportVulkanDrmInterop;
[GeneratedRegex(@"[^\/\\]+?(\.[^\/\\\n.]+)?$")]
private static partial Regex FfprobePathRegex();
/// <summary>
- /// Run at startup or if the user removes a Custom path from transcode page.
+ /// Run at startup to validate ffmpeg.
/// Sets global variables FFmpegPath.
- /// Precedence is: Config > CLI > $PATH.
+ /// Precedence is: CLI/Env var > Config > $PATH.
/// </summary>
- public void SetFFmpegPath()
+ /// <returns>bool indicates whether a valid ffmpeg is found.</returns>
+ public bool SetFFmpegPath()
{
+ var skipValidation = _config.GetFFmpegSkipValidation();
+ if (skipValidation)
+ {
+ _logger.LogWarning("FFmpeg: Skipping FFmpeg Validation due to FFmpeg:novalidation set to true");
+ return true;
+ }
+
// 1) Check if the --ffmpeg CLI switch has been given
var ffmpegPath = _startupOptionFFmpegPath;
+ string ffmpegPathSetMethodText = "command line or environment variable";
if (string.IsNullOrEmpty(ffmpegPath))
{
// 2) Custom path stored in config/encoding xml file under tag <EncoderAppPath> should be used as a fallback
ffmpegPath = _configurationManager.GetEncodingOptions().EncoderAppPath;
+ ffmpegPathSetMethodText = "encoding.xml config file";
if (string.IsNullOrEmpty(ffmpegPath))
{
// 3) Check "ffmpeg"
ffmpegPath = "ffmpeg";
+ ffmpegPathSetMethodText = "system $PATH";
}
}
if (!ValidatePath(ffmpegPath))
{
_ffmpegPath = null;
+ _logger.LogError("FFmpeg: Path set by {FfmpegPathSetMethodText} is invalid", ffmpegPathSetMethodText);
+ return false;
}
// Write the FFmpeg path to the config/encoding.xml file as <EncoderAppPathDisplay> so it appears in UI
@@ -194,7 +217,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_threads = EncodingHelper.GetNumberOfThreads(null, options, null);
- _isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding");
+ _isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding", _ffmpegVersion);
_isLowPriorityHwDecodeSupported = validator.CheckSupportedHwaccelFlag("low_priority");
// Check the Vaapi device vendor
@@ -206,6 +229,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_isVaapiDeviceAmd = validator.CheckVaapiDeviceByDriverName("Mesa Gallium driver", options.VaapiDevice);
_isVaapiDeviceInteliHD = validator.CheckVaapiDeviceByDriverName("Intel iHD driver", options.VaapiDevice);
_isVaapiDeviceInteli965 = validator.CheckVaapiDeviceByDriverName("Intel i965 driver", options.VaapiDevice);
+ _isVaapiDeviceSupportVulkanDrmModifier = validator.CheckVulkanDrmDeviceByExtensionName(options.VaapiDevice, _vulkanImageDrmFmtModifierExts);
_isVaapiDeviceSupportVulkanDrmInterop = validator.CheckVulkanDrmDeviceByExtensionName(options.VaapiDevice, _vulkanExternalMemoryDmaBufExts);
if (_isVaapiDeviceAmd)
@@ -221,6 +245,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
_logger.LogInformation("VAAPI device {RenderNodePath} is Intel GPU (i965)", options.VaapiDevice);
}
+ if (_isVaapiDeviceSupportVulkanDrmModifier)
+ {
+ _logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM modifier", options.VaapiDevice);
+ }
+
if (_isVaapiDeviceSupportVulkanDrmInterop)
{
_logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM interop", options.VaapiDevice);
@@ -229,65 +258,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
_logger.LogInformation("FFmpeg: {FfmpegPath}", _ffmpegPath ?? string.Empty);
- }
-
- /// <summary>
- /// Triggered from the Settings > Transcoding UI page when users submits Custom FFmpeg path to use.
- /// Only write the new path to xml if it exists. Do not perform validation checks on ffmpeg here.
- /// </summary>
- /// <param name="path">The path.</param>
- /// <param name="pathType">The path type.</param>
- public void UpdateEncoderPath(string path, string pathType)
- {
- var config = _configurationManager.GetEncodingOptions();
-
- // Filesystem may not be case insensitive, but EncoderAppPathDisplay should always point to a valid file?
- if (string.IsNullOrEmpty(config.EncoderAppPath)
- && string.Equals(config.EncoderAppPathDisplay, path, StringComparison.OrdinalIgnoreCase))
- {
- _logger.LogDebug("Existing ffmpeg path is empty and the new path is the same as {EncoderAppPathDisplay}. Skipping", nameof(config.EncoderAppPathDisplay));
- return;
- }
-
- string newPath;
-
- _logger.LogInformation("Attempting to update encoder path to {Path}. pathType: {PathType}", path ?? string.Empty, pathType ?? string.Empty);
-
- if (!string.Equals(pathType, "custom", StringComparison.OrdinalIgnoreCase))
- {
- throw new ArgumentException("Unexpected pathType value");
- }
-
- if (string.IsNullOrWhiteSpace(path))
- {
- // User had cleared the custom path in UI
- newPath = string.Empty;
- }
- else
- {
- if (Directory.Exists(path))
- {
- // Given path is directory, so resolve down to filename
- newPath = GetEncoderPathFromDirectory(path, "ffmpeg");
- }
- else
- {
- newPath = path;
- }
-
- if (!new EncoderValidator(_logger, newPath).ValidateVersion())
- {
- throw new ResourceNotFoundException();
- }
- }
-
- // Write the new ffmpeg path to the xml as <EncoderAppPath>
- // This ensures its not lost on next startup
- config.EncoderAppPath = newPath;
- _configurationManager.SaveConfiguration("encoding", config);
-
- // Trigger SetFFmpegPath so we validate the new path and setup probe path
- SetFFmpegPath();
+ return !string.IsNullOrWhiteSpace(ffmpegPath);
}
/// <summary>
@@ -306,7 +277,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
bool rc = new EncoderValidator(_logger, path).ValidateVersion();
if (!rc)
{
- _logger.LogWarning("FFmpeg: Failed version check: {Path}", path);
+ _logger.LogError("FFmpeg: Failed version check: {Path}", path);
return false;
}
@@ -710,18 +681,18 @@ namespace MediaBrowser.MediaEncoding.Encoder
filters.Add("thumbnail=n=" + (useLargerBatchSize ? "50" : "24"));
}
- // Use SW tonemap on HDR10/HLG video stream only when the zscale or tonemapx filter is available.
+ // Use SW tonemap on HDR video stream only when the zscale or tonemapx filter is available.
+ // Only enable Dolby Vision tonemap when tonemapx is available
var enableHdrExtraction = false;
- if (string.Equals(videoStream?.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoStream?.ColorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
+ if (videoStream?.VideoRange == VideoRange.HDR)
{
if (SupportsFilter("tonemapx"))
{
enableHdrExtraction = true;
filters.Add("tonemapx=tonemap=bt2390:desat=0:peak=100:t=bt709:m=bt709:p=bt709:format=yuv420p");
}
- else if (SupportsFilter("zscale"))
+ else if (SupportsFilter("zscale") && videoStream.VideoRangeType != VideoRangeType.DOVI)
{
enableHdrExtraction = true;
filters.Add("zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0:peak=100,zscale=t=bt709:m=bt709,format=yuv420p");
@@ -764,8 +735,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
using (var processWrapper = new ProcessWrapper(process, this))
{
- bool ranToCompletion;
-
using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
{
StartProcess(processWrapper);
@@ -779,22 +748,18 @@ namespace MediaBrowser.MediaEncoding.Encoder
try
{
await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
- ranToCompletion = true;
}
- catch (OperationCanceledException)
+ catch (OperationCanceledException ex)
{
process.Kill(true);
- ranToCompletion = false;
+ throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction timed out for {0} after {1}ms", inputPath, timeoutMs), ex);
}
}
- var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
var file = _fileSystem.GetFileInfo(tempExtractPath);
- if (exitCode == -1 || !file.Exists || file.Length == 0)
+ if (processWrapper.ExitCode > 0 || !file.Exists || file.Length == 0)
{
- _logger.LogError("ffmpeg image extraction failed for {Path}", inputPath);
-
throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", inputPath));
}
diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfoSideData.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfoSideData.cs
index 5dbc438e4..0b5dd1d1b 100644
--- a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfoSideData.cs
+++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfoSideData.cs
@@ -69,5 +69,12 @@ namespace MediaBrowser.MediaEncoding.Probing
/// <value>The DvBlSignalCompatibilityId.</value>
[JsonPropertyName("dv_bl_signal_compatibility_id")]
public int? DvBlSignalCompatibilityId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Rotation in degrees.
+ /// </summary>
+ /// <value>The Rotation.</value>
+ [JsonPropertyName("rotation")]
+ public int? Rotation { get; set; }
}
}
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 5a5eb6e61..334796f58 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -892,8 +892,12 @@ namespace MediaBrowser.MediaEncoding.Probing
stream.ElPresentFlag = data.ElPresentFlag;
stream.BlPresentFlag = data.BlPresentFlag;
stream.DvBlSignalCompatibilityId = data.DvBlSignalCompatibilityId;
+ }
- break;
+ // Parse video rotation metadata from side_data
+ else if (string.Equals(data.SideDataType, "Display Matrix", StringComparison.OrdinalIgnoreCase))
+ {
+ stream.Rotation = data.Rotation;
}
}
}
diff --git a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
index 67a2dddb8..42f355b05 100644
--- a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
+++ b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
@@ -51,6 +51,8 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
o.PoolInitialFill = 1;
});
+ private readonly Version _maxFFmpegCkeyPauseSupported = new Version(6, 1);
+
/// <summary>
/// Initializes a new instance of the <see cref="TranscodeManager"/> class.
/// </summary>
@@ -555,7 +557,9 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
{
- if (EnableThrottling(state))
+ if (EnableThrottling(state)
+ && (_mediaEncoder.IsPkeyPauseSupported
+ || _mediaEncoder.EncoderVersion <= _maxFFmpegCkeyPauseSupported))
{
transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, _loggerFactory.CreateLogger<TranscodingThrottler>(), _serverConfigurationManager, _fileSystem, _mediaEncoder);
transcodingJob.TranscodingThrottler.Start();
diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs
index c956bee47..b0f5c2a11 100644
--- a/MediaBrowser.Model/Configuration/LibraryOptions.cs
+++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs
@@ -13,6 +13,8 @@ namespace MediaBrowser.Model.Configuration
DisabledSubtitleFetchers = Array.Empty<string>();
SubtitleFetcherOrder = Array.Empty<string>();
DisabledLocalMetadataReaders = Array.Empty<string>();
+ DisabledLyricFetchers = Array.Empty<string>();
+ LyricFetcherOrder = Array.Empty<string>();
SkipSubtitlesIfAudioTrackMatches = true;
RequirePerfectSubtitleMatch = true;
@@ -97,6 +99,10 @@ namespace MediaBrowser.Model.Configuration
[DefaultValue(false)]
public bool SaveLyricsWithMedia { get; set; }
+ public string[] DisabledLyricFetchers { get; set; }
+
+ public string[] LyricFetcherOrder { get; set; }
+
public bool AutomaticallyAddToCollection { get; set; }
public EmbeddedSubtitleOptions AllowEmbeddedSubtitles { get; set; }
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index d2715e2ac..1101c76ea 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -965,8 +965,10 @@ namespace MediaBrowser.Model.Dlna
var appliedVideoConditions = options.Profile.CodecProfiles
.Where(i => i.Type == CodecType.Video &&
i.ContainsAnyCodec(videoStream?.Codec, container) &&
- i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)));
- var isFirstAppliedCodecProfile = true;
+ i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)))
+ // Reverse codec profiles for backward compatibility - first codec profile has higher priority
+ .Reverse();
+
foreach (var i in appliedVideoConditions)
{
var transcodingVideoCodecs = ContainerProfile.SplitValue(videoCodec);
@@ -974,8 +976,7 @@ namespace MediaBrowser.Model.Dlna
{
if (i.ContainsAnyCodec(transcodingVideoCodec, container))
{
- ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, isFirstAppliedCodecProfile);
- isFirstAppliedCodecProfile = false;
+ ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, true);
continue;
}
}
@@ -997,8 +998,10 @@ namespace MediaBrowser.Model.Dlna
var appliedAudioConditions = options.Profile.CodecProfiles
.Where(i => i.Type == CodecType.VideoAudio &&
i.ContainsAnyCodec(audioStream?.Codec, container) &&
- i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)));
- isFirstAppliedCodecProfile = true;
+ i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)))
+ // Reverse codec profiles for backward compatibility - first codec profile has higher priority
+ .Reverse();
+
foreach (var codecProfile in appliedAudioConditions)
{
var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec);
@@ -1006,8 +1009,7 @@ namespace MediaBrowser.Model.Dlna
{
if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container))
{
- ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile);
- isFirstAppliedCodecProfile = false;
+ ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, true);
break;
}
}
diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
index b7236b1e8..1c6037325 100644
--- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs
+++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
@@ -117,6 +117,8 @@ namespace MediaBrowser.Model.Dto
public int? DefaultSubtitleStreamIndex { get; set; }
+ public bool HasSegments { get; set; }
+
[JsonIgnore]
public MediaStream VideoStream
{
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
index 20e011745..a0e8c39be 100644
--- a/MediaBrowser.Model/Entities/MediaStream.cs
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -124,6 +124,12 @@ namespace MediaBrowser.Model.Entities
public int? DvBlSignalCompatibilityId { get; set; }
/// <summary>
+ /// Gets or sets the Rotation in degrees.
+ /// </summary>
+ /// <value>The video rotation.</value>
+ public int? Rotation { get; set; }
+
+ /// <summary>
/// Gets or sets the comment.
/// </summary>
/// <value>The comment.</value>
diff --git a/MediaBrowser.Model/Library/UserViewQuery.cs b/MediaBrowser.Model/Library/UserViewQuery.cs
index e20d6af49..643a1f9b1 100644
--- a/MediaBrowser.Model/Library/UserViewQuery.cs
+++ b/MediaBrowser.Model/Library/UserViewQuery.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
namespace MediaBrowser.Model.Library
@@ -14,10 +15,10 @@ namespace MediaBrowser.Model.Library
}
/// <summary>
- /// Gets or sets the user identifier.
+ /// Gets or sets the user.
/// </summary>
- /// <value>The user identifier.</value>
- public Guid UserId { get; set; }
+ /// <value>The user.</value>
+ public required User User { get; set; }
/// <summary>
/// Gets or sets a value indicating whether [include external content].
diff --git a/MediaBrowser.Model/MediaSegments/MediaSegmentDto.cs b/MediaBrowser.Model/MediaSegments/MediaSegmentDto.cs
new file mode 100644
index 000000000..a0433fee1
--- /dev/null
+++ b/MediaBrowser.Model/MediaSegments/MediaSegmentDto.cs
@@ -0,0 +1,35 @@
+using System;
+using Jellyfin.Data.Enums;
+
+namespace MediaBrowser.Model.MediaSegments;
+
+/// <summary>
+/// Api model for MediaSegment's.
+/// </summary>
+public class MediaSegmentDto
+{
+ /// <summary>
+ /// Gets or sets the id of the media segment.
+ /// </summary>
+ public Guid Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id of the associated item.
+ /// </summary>
+ public Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of content this segment defines.
+ /// </summary>
+ public MediaSegmentType Type { get; set; }
+
+ /// <summary>
+ /// Gets or sets the start of the segment.
+ /// </summary>
+ public long StartTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the end of the segment.
+ /// </summary>
+ public long EndTicks { get; set; }
+}
diff --git a/MediaBrowser.Model/Plugins/PluginPageInfo.cs b/MediaBrowser.Model/Plugins/PluginPageInfo.cs
index f4d83c28b..2ab93ea05 100644
--- a/MediaBrowser.Model/Plugins/PluginPageInfo.cs
+++ b/MediaBrowser.Model/Plugins/PluginPageInfo.cs
@@ -6,12 +6,12 @@ namespace MediaBrowser.Model.Plugins
public class PluginPageInfo
{
/// <summary>
- /// Gets or sets the name.
+ /// Gets or sets the name of the plugin.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
- /// Gets or sets the display name.
+ /// Gets or sets the display name of the plugin.
/// </summary>
public string? DisplayName { get; set; }
diff --git a/MediaBrowser.Model/Querying/LatestItemsQuery.cs b/MediaBrowser.Model/Querying/LatestItemsQuery.cs
index d2d9f1f9a..251ff5d68 100644
--- a/MediaBrowser.Model/Querying/LatestItemsQuery.cs
+++ b/MediaBrowser.Model/Querying/LatestItemsQuery.cs
@@ -2,6 +2,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities;
@@ -18,7 +19,7 @@ namespace MediaBrowser.Model.Querying
/// Gets or sets the user to localize search results for.
/// </summary>
/// <value>The user id.</value>
- public Guid UserId { get; set; }
+ public User User { get; set; }
/// <summary>
/// Gets or sets the parent id.
diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs
index 35353e6fa..8dece28a0 100644
--- a/MediaBrowser.Model/Querying/NextUpQuery.cs
+++ b/MediaBrowser.Model/Querying/NextUpQuery.cs
@@ -1,7 +1,7 @@
-#nullable disable
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Entities;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Model.Querying
@@ -19,10 +19,10 @@ namespace MediaBrowser.Model.Querying
}
/// <summary>
- /// Gets or sets the user id.
+ /// Gets or sets the user.
/// </summary>
- /// <value>The user id.</value>
- public Guid UserId { get; set; }
+ /// <value>The user.</value>
+ public required User User { get; set; }
/// <summary>
/// Gets or sets the parent identifier.
@@ -49,24 +49,6 @@ namespace MediaBrowser.Model.Querying
public int? Limit { get; set; }
/// <summary>
- /// gets or sets the fields to return within the items, in addition to basic information.
- /// </summary>
- /// <value>The fields.</value>
- public ItemFields[] Fields { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [enable images].
- /// </summary>
- /// <value><c>null</c> if [enable images] contains no value, <c>true</c> if [enable images]; otherwise, <c>false</c>.</value>
- public bool? EnableImages { get; set; }
-
- /// <summary>
- /// Gets or sets the image type limit.
- /// </summary>
- /// <value>The image type limit.</value>
- public int? ImageTypeLimit { get; set; }
-
- /// <summary>
/// Gets or sets the enable image types.
/// </summary>
/// <value>The enable image types.</value>
diff --git a/MediaBrowser.Providers/Lyric/LrcLyricParser.cs b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs
index 67b26e457..fffdf4887 100644
--- a/MediaBrowser.Providers/Lyric/LrcLyricParser.cs
+++ b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
using System.Linq;
using Jellyfin.Extensions;
@@ -20,7 +19,6 @@ public class LrcLyricParser : ILyricParser
private readonly LyricParser _lrcLyricParser;
private static readonly string[] _supportedMediaTypes = [".lrc", ".elrc"];
- private static readonly string[] _acceptedTimeFormats = ["HH:mm:ss", "H:mm:ss", "mm:ss", "m:ss"];
/// <summary>
/// Initializes a new instance of the <see cref="LrcLyricParser"/> class.
@@ -59,37 +57,7 @@ public class LrcLyricParser : ILyricParser
return null;
}
- List<LrcParser.Model.Lyric> sortedLyricData = lyricData.Lyrics.Where(x => x.TimeTags.Count > 0).OrderBy(x => x.TimeTags.First().Value).ToList();
-
- // Parse metadata rows
- var metaDataRows = lyricData.Lyrics
- .Where(x => x.TimeTags.Count == 0)
- .Where(x => x.Text.StartsWith('[') && x.Text.EndsWith(']'))
- .Select(x => x.Text)
- .ToList();
-
- var fileMetaData = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- foreach (string metaDataRow in metaDataRows)
- {
- var index = metaDataRow.IndexOf(':', StringComparison.OrdinalIgnoreCase);
- if (index == -1)
- {
- continue;
- }
-
- // Remove square bracket before field name, and after field value
- // Example 1: [au: 1hitsong]
- // Example 2: [ar: Calabrese]
- var metaDataFieldName = GetMetadataFieldName(metaDataRow, index);
- var metaDataFieldValue = GetMetadataValue(metaDataRow, index);
-
- if (string.IsNullOrEmpty(metaDataFieldName) || string.IsNullOrEmpty(metaDataFieldValue))
- {
- continue;
- }
-
- fileMetaData[metaDataFieldName] = metaDataFieldValue;
- }
+ List<LrcParser.Model.Lyric> sortedLyricData = lyricData.Lyrics.OrderBy(x => x.StartTime).ToList();
if (sortedLyricData.Count == 0)
{
@@ -100,99 +68,10 @@ public class LrcLyricParser : ILyricParser
for (int i = 0; i < sortedLyricData.Count; i++)
{
- var timeData = sortedLyricData[i].TimeTags.First().Value;
- if (timeData is null)
- {
- continue;
- }
-
- long ticks = TimeSpan.FromMilliseconds(timeData.Value).Ticks;
+ long ticks = TimeSpan.FromMilliseconds(sortedLyricData[i].StartTime).Ticks;
lyricList.Add(new LyricLine(sortedLyricData[i].Text.Trim(), ticks));
}
- if (fileMetaData.Count != 0)
- {
- // Map metaData values from LRC file to LyricMetadata properties
- LyricMetadata lyricMetadata = MapMetadataValues(fileMetaData);
-
- return new LyricDto { Metadata = lyricMetadata, Lyrics = lyricList };
- }
-
return new LyricDto { Lyrics = lyricList };
}
-
- /// <summary>
- /// Converts metadata from an LRC file to LyricMetadata properties.
- /// </summary>
- /// <param name="metaData">The metadata from the LRC file.</param>
- /// <returns>A lyricMetadata object with mapped property data.</returns>
- private static LyricMetadata MapMetadataValues(Dictionary<string, string> metaData)
- {
- LyricMetadata lyricMetadata = new();
-
- if (metaData.TryGetValue("ar", out var artist) && !string.IsNullOrEmpty(artist))
- {
- lyricMetadata.Artist = artist;
- }
-
- if (metaData.TryGetValue("al", out var album) && !string.IsNullOrEmpty(album))
- {
- lyricMetadata.Album = album;
- }
-
- if (metaData.TryGetValue("ti", out var title) && !string.IsNullOrEmpty(title))
- {
- lyricMetadata.Title = title;
- }
-
- if (metaData.TryGetValue("au", out var author) && !string.IsNullOrEmpty(author))
- {
- lyricMetadata.Author = author;
- }
-
- if (metaData.TryGetValue("length", out var length) && !string.IsNullOrEmpty(length))
- {
- if (DateTime.TryParseExact(length, _acceptedTimeFormats, null, DateTimeStyles.None, out var value))
- {
- lyricMetadata.Length = value.TimeOfDay.Ticks;
- }
- }
-
- if (metaData.TryGetValue("by", out var by) && !string.IsNullOrEmpty(by))
- {
- lyricMetadata.By = by;
- }
-
- if (metaData.TryGetValue("offset", out var offset) && !string.IsNullOrEmpty(offset))
- {
- if (int.TryParse(offset, out var value))
- {
- lyricMetadata.Offset = TimeSpan.FromMilliseconds(value).Ticks;
- }
- }
-
- if (metaData.TryGetValue("re", out var creator) && !string.IsNullOrEmpty(creator))
- {
- lyricMetadata.Creator = creator;
- }
-
- if (metaData.TryGetValue("ve", out var version) && !string.IsNullOrEmpty(version))
- {
- lyricMetadata.Version = version;
- }
-
- return lyricMetadata;
- }
-
- private static string GetMetadataFieldName(string metaDataRow, int index)
- {
- var metadataFieldName = metaDataRow.AsSpan(1, index - 1).Trim();
- return metadataFieldName.IsEmpty ? string.Empty : metadataFieldName.ToString();
- }
-
- private static string GetMetadataValue(string metaDataRow, int index)
- {
- var metadataValue = metaDataRow.AsSpan(index + 1, metaDataRow.Length - index - 2).Trim();
- return metadataValue.IsEmpty ? string.Empty : metadataValue.ToString();
- }
}
diff --git a/MediaBrowser.Providers/Lyric/LyricScheduledTask.cs b/MediaBrowser.Providers/Lyric/LyricScheduledTask.cs
new file mode 100644
index 000000000..1a4136504
--- /dev/null
+++ b/MediaBrowser.Providers/Lyric/LyricScheduledTask.cs
@@ -0,0 +1,170 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Lyrics;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Lyrics;
+using MediaBrowser.Model.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Lyric;
+
+/// <summary>
+/// Task to download lyrics.
+/// </summary>
+public class LyricScheduledTask : IScheduledTask
+{
+ private const int QueryPageLimit = 100;
+
+ private static readonly BaseItemKind[] _itemKinds = [BaseItemKind.Audio];
+ private static readonly MediaType[] _mediaTypes = [MediaType.Audio];
+ private static readonly SourceType[] _sourceTypes = [SourceType.Library];
+ private static readonly DtoOptions _dtoOptions = new(false);
+
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILyricManager _lyricManager;
+ private readonly ILogger<LyricScheduledTask> _logger;
+ private readonly ILocalizationManager _localizationManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LyricScheduledTask"/> class.
+ /// </summary>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{DownloaderScheduledTask}"/> interface.</param>
+ /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ public LyricScheduledTask(
+ ILibraryManager libraryManager,
+ ILyricManager lyricManager,
+ ILogger<LyricScheduledTask> logger,
+ ILocalizationManager localizationManager)
+ {
+ _libraryManager = libraryManager;
+ _lyricManager = lyricManager;
+ _logger = logger;
+ _localizationManager = localizationManager;
+ }
+
+ /// <inheritdoc />
+ public string Name => _localizationManager.GetLocalizedString("TaskDownloadMissingLyrics");
+
+ /// <inheritdoc />
+ public string Key => "DownloadLyrics";
+
+ /// <inheritdoc />
+ public string Description => _localizationManager.GetLocalizedString("TaskDownloadMissingLyricsDescription");
+
+ /// <inheritdoc />
+ public string Category => _localizationManager.GetLocalizedString("TasksLibraryCategory");
+
+ /// <inheritdoc />
+ public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var totalCount = _libraryManager.GetCount(new InternalItemsQuery
+ {
+ Recursive = true,
+ IsVirtualItem = false,
+ IncludeItemTypes = _itemKinds,
+ DtoOptions = _dtoOptions,
+ MediaTypes = _mediaTypes,
+ SourceTypes = _sourceTypes
+ });
+
+ var completed = 0;
+
+ foreach (var library in _libraryManager.RootFolder.Children.ToList())
+ {
+ var libraryOptions = _libraryManager.GetLibraryOptions(library);
+ var itemQuery = new InternalItemsQuery
+ {
+ Recursive = true,
+ IsVirtualItem = false,
+ IncludeItemTypes = _itemKinds,
+ DtoOptions = _dtoOptions,
+ MediaTypes = _mediaTypes,
+ SourceTypes = _sourceTypes,
+ Limit = QueryPageLimit,
+ Parent = library
+ };
+
+ int previousCount;
+ var startIndex = 0;
+ do
+ {
+ itemQuery.StartIndex = startIndex;
+ var audioItems = _libraryManager.GetItemList(itemQuery);
+
+ foreach (var audioItem in audioItems.OfType<Audio>())
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ if (audioItem.GetMediaStreams().All(s => s.Type != MediaStreamType.Lyric))
+ {
+ _logger.LogDebug("Searching for lyrics for {Path}", audioItem.Path);
+ var lyricResults = await _lyricManager.SearchLyricsAsync(
+ new LyricSearchRequest
+ {
+ MediaPath = audioItem.Path,
+ SongName = audioItem.Name,
+ AlbumName = audioItem.Album,
+ ArtistNames = audioItem.GetAllArtists().ToList(),
+ Duration = audioItem.RunTimeTicks,
+ IsAutomated = true,
+ DisabledLyricFetchers = libraryOptions.DisabledLyricFetchers,
+ LyricFetcherOrder = libraryOptions.LyricFetcherOrder
+ },
+ cancellationToken)
+ .ConfigureAwait(false);
+
+ if (lyricResults.Count != 0)
+ {
+ _logger.LogDebug("Saving lyrics for {Path}", audioItem.Path);
+ await _lyricManager.DownloadLyricsAsync(
+ audioItem,
+ libraryOptions,
+ lyricResults[0].Id,
+ cancellationToken)
+ .ConfigureAwait(false);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error downloading lyrics for {Path}", audioItem.Path);
+ }
+
+ completed++;
+ progress.Report(100d * completed / totalCount);
+ }
+
+ startIndex += QueryPageLimit;
+ previousCount = audioItems.Count;
+ } while (previousCount > 0);
+ }
+
+ progress.Report(100);
+ }
+
+ /// <inheritdoc />
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ return
+ [
+ new TaskTriggerInfo
+ {
+ Type = TaskTriggerInfo.TriggerInterval,
+ IntervalTicks = TimeSpan.FromHours(24).Ticks
+ }
+ ];
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs b/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs
index 78150153a..28f8c0c61 100644
--- a/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs
@@ -18,7 +18,7 @@ namespace MediaBrowser.Providers.Plugins.StudioImages
/// <summary>
/// Artwork repository URL.
/// </summary>
- public const string DefaultServer = "https://raw.github.com/jellyfin/emby-artwork/master/studios";
+ public const string DefaultServer = "https://raw.githubusercontent.com/jellyfin/emby-artwork/master/studios";
/// <summary>
/// Initializes a new instance of the <see cref="Plugin"/> class.
diff --git a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
index a8461e991..5ca9f6f9a 100644
--- a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
@@ -53,10 +53,7 @@ namespace MediaBrowser.Providers.Plugins.StudioImages
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
- return new ImageType[]
- {
- ImageType.Thumb
- };
+ return [ImageType.Thumb];
}
/// <inheritdoc />
@@ -72,13 +69,10 @@ namespace MediaBrowser.Providers.Plugins.StudioImages
if (imageInfo is null)
{
- return Enumerable.Empty<RemoteImageInfo>();
+ return [];
}
- return new RemoteImageInfo[]
- {
- imageInfo
- };
+ return [imageInfo];
}
private RemoteImageInfo GetImage(BaseItem item, string filename, ImageType type, string remoteFilename)
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index b03d6ffb5..80c56351c 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -61,8 +61,8 @@ namespace MediaBrowser.Providers.TV
await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
RemoveObsoleteEpisodes(item);
- await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
RemoveObsoleteSeasons(item);
+ await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -211,8 +211,12 @@ namespace MediaBrowser.Providers.TV
}
else if (existingSeason.IsVirtualItem)
{
- existingSeason.IsVirtualItem = false;
- await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
+ var episodeCount = seriesChildren.OfType<Episode>().Count(e => e.ParentIndexNumber == seasonNumber && !e.IsMissingEpisode);
+ if (episodeCount > 0)
+ {
+ existingSeason.IsVirtualItem = false;
+ await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
+ }
}
}
}
diff --git a/README.md b/README.md
index e5e16c716..7da0cb30d 100644
--- a/README.md
+++ b/README.md
@@ -82,7 +82,7 @@ Instructions to run this project from the command line are included here, but yo
### Cloning the Repository
-After dependencies are installed you will need to clone a local copy of this repository. If you just want to run the server from source you can clone this repository directly, but if you are intending to contribute code changes to the project, you should [set up your own fork](https://jellyfin.org/docs/general/contributing/development.html#set-up-your-copy-of-the-repo) of the repository. The following example shows how you can clone the repository directly over HTTPS.
+After dependencies have been installed you will need to clone a local copy of this repository. If you just want to run the server from source you can clone this repository directly, but if you are intending to contribute code changes to the project, you should [set up your own fork](https://jellyfin.org/docs/general/contributing/development.html#set-up-your-copy-of-the-repo) of the repository. The following example shows how you can clone the repository directly over HTTPS.
```bash
git clone https://github.com/jellyfin/jellyfin.git
@@ -116,7 +116,7 @@ Second, you need to [install the recommended extensions for the workspace](https
After the required extensions are installed, you can run the server by pressing `F5`.
-#### Running From The Command Line
+#### Running From the Command Line
To run the server from the command line you can use the `dotnet run` command. The example below shows how to do this if you have cloned the repository into a directory named `jellyfin` (the default directory name) and should work on all operating systems.
@@ -143,9 +143,9 @@ If the Server is configured to host the Web Client, and the Server is running, t
API documentation can be viewed at `http://localhost:8096/api-docs/swagger/index.html`
-### Running from GH-Codespaces
+### Running from GitHub Codespaces
-As Jellyfin will run on a container on a github hosted server, JF needs to handle some things differently.
+As Jellyfin will run on a container on a GitHub hosted server, JF needs to handle some things differently.
**NOTE:** Depending on the selected configuration (if you just click 'create codespace' it will create a default configuration one) it might take 20-30 seconds to load all extensions and prepare the environment while VS Code is already open. Just give it some time and wait until you see `Downloading .NET version(s) 7.0.15~x64 ...... Done!` in the output tab.
@@ -182,12 +182,12 @@ The following sections describe some more advanced scenarios for running the ser
It is not necessary to host the frontend web client as part of the backend server. Hosting these two components separately may be useful for frontend developers who would prefer to host the client in a separate webpack development server for a tighter development loop. See the [jellyfin-web](https://github.com/jellyfin/jellyfin-web#getting-started) repo for instructions on how to do this.
-To instruct the server not to host the web content, there is a `nowebclient` configuration flag that must be set. This can specified using the command line
+To instruct the server not to host the web content, there is a `nowebclient` configuration flag that must be set. This can be specified using the command line
switch `--nowebclient` or the environment variable `JELLYFIN_NOWEBCONTENT=true`.
Since this is a common scenario, there is also a separate launch profile defined for Visual Studio called `Jellyfin.Server (nowebcontent)` that can be selected from the 'Start Debugging' dropdown in the main toolbar.
-**NOTE:** The setup wizard can not be run if the web client is hosted separately.
+**NOTE:** The setup wizard cannot be run if the web client is hosted separately.
---
<p align="center">
diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs
index 213328a39..5d4732234 100644
--- a/src/Jellyfin.Drawing/ImageProcessor.cs
+++ b/src/Jellyfin.Drawing/ImageProcessor.cs
@@ -102,7 +102,8 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
"astc",
"ktx",
"pkm",
- "wbmp"
+ "wbmp",
+ "avif"
};
/// <inheritdoc />
diff --git a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs
index cecc363f0..7dc30f727 100644
--- a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs
@@ -167,7 +167,7 @@ namespace Jellyfin.LiveTv.Listings
Overview = program.Description,
ProductionYear = program.CopyrightDate?.Year,
SeasonNumber = program.Episode.Series,
- IsSeries = program.Episode.Series is not null,
+ IsSeries = program.Episode.Episode is not null,
IsRepeat = program.IsPreviouslyShown && !program.IsNew,
IsPremiere = program.Premiere is not null,
IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
index e0a7fa3aa..988073074 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
@@ -17,6 +17,7 @@ namespace Jellyfin.MediaEncoding.Tests
}
[Theory]
+ [InlineData(EncoderValidatorTestsData.FFmpegV701Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV611Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV60Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV512Output, true)]
@@ -33,6 +34,7 @@ namespace Jellyfin.MediaEncoding.Tests
{
public GetFFmpegVersionTestData()
{
+ Add(EncoderValidatorTestsData.FFmpegV701Output, new Version(7, 0, 1));
Add(EncoderValidatorTestsData.FFmpegV611Output, new Version(6, 1, 1));
Add(EncoderValidatorTestsData.FFmpegV60Output, new Version(6, 0));
Add(EncoderValidatorTestsData.FFmpegV512Output, new Version(5, 1, 2));
diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
index 30df94950..1f2d618aa 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
@@ -2,6 +2,18 @@ namespace Jellyfin.MediaEncoding.Tests
{
internal static class EncoderValidatorTestsData
{
+ public const string FFmpegV701Output = @"ffmpeg version 7.0.1-Jellyfin Copyright (c) 2000-2024 the FFmpeg developers
+built with clang version 18.1.8
+configuration: --cc=clang --pkg-config-flags=--static --extra-cflags=-I/clang64/ffbuild/include --extra-ldflags=-L/clang64/ffbuild/lib --prefix=/clang64/ffbuild/jellyfin-ffmpeg --extra-version=Jellyfin --disable-ffplay --disable-debug --disable-doc --disable-sdl2 --disable-ptx-compression --enable-lto=thin --enable-gpl --enable-version3 --enable-schannel --enable-iconv --enable-libxml2 --enable-zlib --enable-lzma --enable-gmp --enable-chromaprint --enable-libfreetype --enable-libfribidi --enable-libfontconfig --enable-libharfbuzz --enable-libass --enable-libbluray --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libopenmpt --enable-libwebp --enable-libvpx --enable-libzimg --enable-libx264 --enable-libx265 --enable-libsvtav1 --enable-libdav1d --enable-libfdk-aac --enable-opencl --enable-dxva2 --enable-d3d11va --enable-amf --enable-libvpl --enable-ffnvcodec --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvdec --enable-nvenc
+libavutil 59. 8.100 / 59. 8.100
+libavcodec 61. 3.100 / 61. 3.100
+libavformat 61. 1.100 / 61. 1.100
+libavdevice 61. 1.100 / 61. 1.100
+libavfilter 10. 1.100 / 10. 1.100
+libswscale 8. 1.100 / 8. 1.100
+libswresample 5. 1.100 / 5. 1.100
+libpostproc 58. 1.100 / 58. 1.100";
+
public const string FFmpegV611Output = @"ffmpeg version n6.1.1-16-g33efa50fa4-20240317 Copyright (c) 2000-2023 the FFmpeg developers
built with gcc 13.2.0 (crosstool-NG 1.26.0.65_ecc5e41)
configuration: --prefix=/ffbuild/prefix --pkg-config-flags=--static --pkg-config=pkg-config --cross-prefix=x86_64-w64-mingw32- --arch=x86_64 --target-os=mingw32 --enable-gpl --enable-version3 --disable-debug --enable-shared --disable-static --disable-w32threads --enable-pthreads --enable-iconv --enable-libxml2 --enable-zlib --enable-libfreetype --enable-libfribidi --enable-gmp --enable-lzma --enable-fontconfig --enable-libharfbuzz --enable-libvorbis --enable-opencl --disable-libpulse --enable-libvmaf --disable-libxcb --disable-xlib --enable-amf --enable-libaom --enable-libaribb24 --enable-avisynth --enable-chromaprint --enable-libdav1d --enable-libdavs2 --disable-libfdk-aac --enable-ffnvcodec --enable-cuda-llvm --enable-frei0r --enable-libgme --enable-libkvazaar --enable-libaribcaption --enable-libass --enable-libbluray --enable-libjxl --enable-libmp3lame --enable-libopus --enable-librist --enable-libssh --enable-libtheora --enable-libvpx --enable-libwebp --enable-lv2 --enable-libvpl --enable-openal --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenh264 --enable-libopenjpeg --enable-libopenmpt --enable-librav1e --enable-librubberband --enable-schannel --enable-sdl2 --enable-libsoxr --enable-libsrt --enable-libsvtav1 --enable-libtwolame --enable-libuavs3d --disable-libdrm --enable-vaapi --enable-libvidstab --enable-vulkan --enable-libshaderc --enable-libplacebo --enable-libx264 --enable-libx265 --enable-libxavs2 --enable-libxvid --enable-libzimg --enable-libzvbi --extra-cflags='$FF_CFLAGS' --extra-cxxflags='$FF_CXXFLAGS' --extra-ldflags='$FF_LDFLAGS' --extra-ldexeflags='$FF_LDEXEFLAGS' --extra-libs='$FF_LIBS' --extra-version=20240317
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
index 612064190..df51d39cb 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
@@ -84,6 +84,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.Equal(0, res.VideoStream.ElPresentFlag);
Assert.Equal(1, res.VideoStream.BlPresentFlag);
Assert.Equal(0, res.VideoStream.DvBlSignalCompatibilityId);
+ Assert.Equal(-180, res.VideoStream.Rotation);
var audio1 = res.MediaStreams[1];
Assert.Equal("eac3", audio1.Codec);
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_metadata.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_metadata.json
index a49c68690..df41ab16e 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_metadata.json
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_metadata.json
@@ -59,6 +59,10 @@
"el_present_flag": 0,
"bl_present_flag": 1,
"dv_bl_signal_compatibility_id": 0
+ },
+ {
+ "side_data_type": "Display Matrix",
+ "rotation": -180
}
]
},
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/LiveTvControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/LiveTvControllerTests.cs
new file mode 100644
index 000000000..dd971fa87
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/LiveTvControllerTests.cs
@@ -0,0 +1,96 @@
+using System.Net;
+using System.Net.Http.Json;
+using System.Net.Mime;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Model.LiveTv;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public sealed class LiveTvControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+ private readonly JellyfinApplicationFactory _factory;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+ private static string? _accessToken;
+
+ public LiveTvControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task AddTunerHost_Unauthorized_ReturnsUnauthorized()
+ {
+ var client = _factory.CreateClient();
+
+ var body = new TunerHostInfo()
+ {
+ Type = "m3u",
+ Url = "Test Data/dummy.m3u8"
+ };
+
+ var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions);
+
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task AddTunerHost_Valid_ReturnsCorrectResponse()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
+
+ var body = new TunerHostInfo()
+ {
+ Type = "m3u",
+ Url = "Test Data/dummy.m3u8"
+ };
+
+ var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions);
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
+ Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
+ var responseBody = await response.Content.ReadFromJsonAsync<TunerHostInfo>();
+ Assert.NotNull(responseBody);
+ Assert.Equal(body.Type, responseBody.Type);
+ Assert.Equal(body.Url, responseBody.Url);
+ }
+
+ [Fact]
+ public async Task AddTunerHost_InvalidType_ReturnsNotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
+
+ var body = new TunerHostInfo()
+ {
+ Type = "invalid",
+ Url = "Test Data/dummy.m3u8"
+ };
+
+ var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions);
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task AddTunerHost_InvalidUrl_ReturnsNotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
+
+ var body = new TunerHostInfo()
+ {
+ Type = "m3u",
+ Url = "thisgoesnowhere"
+ };
+
+ var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions);
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/PluginsControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/PluginsControllerTests.cs
new file mode 100644
index 000000000..547bfcc0f
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/PluginsControllerTests.cs
@@ -0,0 +1,45 @@
+using System.Net;
+using System.Net.Http.Json;
+using System.Net.Mime;
+using System.Text;
+using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Model.Plugins;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public sealed class PluginsControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+ private readonly JellyfinApplicationFactory _factory;
+ private static string? _accessToken;
+
+ public PluginsControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task GetPlugins_Unauthorized_ReturnsUnauthorized()
+ {
+ var client = _factory.CreateClient();
+
+ var response = await client.GetAsync("/Plugins");
+
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task GetPlugins_Authorized_ReturnsCorrectResponse()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
+
+ var response = await client.GetAsync("/Plugins");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
+ Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
+ _ = await response.Content.ReadFromJsonAsync<PluginInfo[]>(JsonDefaults.Options);
+ }
+}
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 a5296d8c9..8228c0df7 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
+++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
@@ -18,6 +18,9 @@
</ItemGroup>
<ItemGroup>
+ <None Include="Test Data\**\*.*">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
<!-- Don't run tests in parallel -->
<None Update="xunit.runner.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
index a078eff77..78b32d278 100644
--- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
@@ -47,6 +47,8 @@ namespace Jellyfin.Server.Integration.Tests
/// <inheritdoc/>
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
+ // Skip ffmpeg check for testing
+ Environment.SetEnvironmentVariable("JELLYFIN_FFMPEG__NOVALIDATION", "true");
// Specify the startup command line options
var commandLineOpts = new StartupOptions();
diff --git a/tests/Jellyfin.Server.Integration.Tests/Test Data/dummy.m3u8 b/tests/Jellyfin.Server.Integration.Tests/Test Data/dummy.m3u8
new file mode 100644
index 000000000..7f60f38a6
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Test Data/dummy.m3u8
@@ -0,0 +1 @@
+C:\Music