aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/dependabot.yml15
-rw-r--r--.github/renovate.json6
-rw-r--r--.github/workflows/automation.yml12
-rw-r--r--.github/workflows/codeql-analysis.yml10
-rw-r--r--.github/workflows/commands.yml16
-rw-r--r--.github/workflows/openapi.yml22
-rw-r--r--.github/workflows/repo-stale.yaml2
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs13
-rw-r--r--Emby.Server.Implementations/Archiving/ZipClient.cs46
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj9
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs6
-rw-r--r--Emby.Server.Implementations/Library/MediaStreamSelector.cs32
-rw-r--r--Emby.Server.Implementations/Library/ResolverHelper.cs7
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs5
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/GenericFolderResolver.cs1
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs58
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs1
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs6
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs114
-rw-r--r--Emby.Server.Implementations/Localization/Core/ar.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/cs.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/el.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-GB.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-AR.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/eu.json9
-rw-r--r--Emby.Server.Implementations/Localization/Core/fi.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr-CA.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/gl.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/he.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/hu.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/id.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/jbo.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/km.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-BR.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/sq.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/uk.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-CN.json3
-rw-r--r--Jellyfin.Api/Controllers/PlaystateController.cs4
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs4
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs2
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj2
-rw-r--r--Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj4
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj8
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj4
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs68
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs281
-rw-r--r--MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs7
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs6
-rw-r--r--MediaBrowser.Controller/Properties/AssemblyInfo.cs3
-rw-r--r--MediaBrowser.Controller/Resolvers/ItemResolver.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs42
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs19
-rw-r--r--MediaBrowser.Model/IO/IZipClient.cs16
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj1
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs65
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs33
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs33
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs878
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs33
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs325
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs33
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs33
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs33
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs71
-rw-r--r--deployment/Dockerfile.centos.amd642
-rw-r--r--deployment/Dockerfile.fedora.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.arm642
-rw-r--r--deployment/Dockerfile.ubuntu.armhf2
-rw-r--r--fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj2
-rw-r--r--jellyfin.ruleset10
-rw-r--r--src/Jellyfin.Extensions/EnumerableExtensions.cs70
-rw-r--r--tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs18
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs2
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs2
81 files changed, 1085 insertions, 1508 deletions
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index 70bcd4973..000000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-version: 2
-updates:
-- package-ecosystem: nuget
- directory: "/"
- schedule:
- interval: weekly
- time: '12:00'
- open-pull-requests-limit: 10
-
-- package-ecosystem: github-actions
- directory: '/'
- schedule:
- interval: weekly
- time: '12:00'
- open-pull-requests-limit: 10
diff --git a/.github/renovate.json b/.github/renovate.json
new file mode 100644
index 000000000..5ca683876
--- /dev/null
+++ b/.github/renovate.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "extends": [
+ "github>jellyfin/.github//renovate-presets/dotnet"
+ ]
+}
diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml
index 20294843d..01cd41a08 100644
--- a/.github/workflows/automation.yml
+++ b/.github/workflows/automation.yml
@@ -14,7 +14,7 @@ jobs:
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Apply label
- uses: eps1lon/actions-label-merge-conflict@v2.0.1
+ uses: eps1lon/actions-label-merge-conflict@b8bf8341285ec9a4567d4318ba474fee998a6919 # tag=v2.0.1
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with:
dirtyLabel: 'merge conflict'
@@ -26,7 +26,7 @@ jobs:
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Remove from 'Current Release' project
- uses: alex-page/github-project-automation-plus@v0.8.1
+ uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true
with:
@@ -35,7 +35,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Release Next' project
- uses: alex-page/github-project-automation-plus@v0.8.1
+ uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
continue-on-error: true
with:
@@ -44,7 +44,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Current Release' project
- uses: alex-page/github-project-automation-plus@v0.8.1
+ uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true
with:
@@ -58,7 +58,7 @@ jobs:
run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
- name: Move issue to needs triage
- uses: alex-page/github-project-automation-plus@v0.8.1
+ uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
continue-on-error: true
with:
@@ -67,7 +67,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add issue to triage project
- uses: alex-page/github-project-automation-plus@v0.8.1
+ uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
if: github.event.issue.pull_request == '' && github.event.action == 'opened'
continue-on-error: true
with:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 1dbd7fa36..b551bb5a6 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v3
+ uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
- name: Setup .NET Core
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@4d4a70f4a5b2a5a5329f13be4ac933f2c9206ac0 # tag=v3
with:
dotnet-version: '6.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@v2
+ uses: github/codeql-action/init@cc7986c02bac29104a72998e67239bb5ee2ee110 # tag=v2
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@v2
+ uses: github/codeql-action/autobuild@cc7986c02bac29104a72998e67239bb5ee2ee110 # tag=v2
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
+ uses: github/codeql-action/analyze@cc7986c02bac29104a72998e67239bb5ee2ee110 # tag=v2
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index 23873706d..a29519b29 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -16,20 +16,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
- uses: peter-evans/create-or-update-comment@v2
+ uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
reactions: '+1'
- name: Checkout the latest code
- uses: actions/checkout@v3
+ uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Automatic Rebase
- uses: cirrus-actions/rebase@1.7
+ uses: cirrus-actions/rebase@6e572f08c244e2f04f9beb85a943eb618218714d # tag=1.7
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
@@ -39,7 +39,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
- uses: peter-evans/create-or-update-comment@v2
+ uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -47,14 +47,14 @@ jobs:
reactions: eyes
- name: Checkout the latest code
- uses: actions/checkout@v3
+ uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Notify as running
id: comment_running
- uses: peter-evans/create-or-update-comment@v2
+ uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -89,7 +89,7 @@ jobs:
exit ${retcode}
- name: Notify with result success
- uses: peter-evans/create-or-update-comment@v2
+ uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
if: ${{ github.event.comment != null && success() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -104,7 +104,7 @@ jobs:
reactions: hooray
- name: Notify with result failure
- uses: peter-evans/create-or-update-comment@v2
+ uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
if: ${{ github.event.comment != null && failure() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml
index ceb4e8cdf..7151d329c 100644
--- a/.github/workflows/openapi.yml
+++ b/.github/workflows/openapi.yml
@@ -12,18 +12,18 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@v3
+ uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET Core
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@4d4a70f4a5b2a5a5329f13be4ac933f2c9206ac0 # tag=v3
with:
dotnet-version: '6.0.x'
- 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@v3
+ uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3
with:
name: openapi-head
retention-days: 14
@@ -37,17 +37,17 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@v3
+ uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
with:
ref: ${{ github.base_ref }}
- name: Setup .NET Core
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@4d4a70f4a5b2a5a5329f13be4ac933f2c9206ac0 # tag=v3
with:
dotnet-version: '6.0.x'
- 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@v3
+ uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3
with:
name: openapi-base
retention-days: 14
@@ -63,12 +63,12 @@ jobs:
- openapi-base
steps:
- name: Download openapi-head
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741 # tag=v3
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741 # tag=v3
with:
name: openapi-base
path: openapi-base
@@ -90,14 +90,14 @@ jobs:
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
- name: Find difference comment
- uses: peter-evans/find-comment@v2
+ uses: peter-evans/find-comment@b657a70ff16d17651703a84bee1cb9ad9d2be2ea # tag=v2
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
direction: last
body-includes: openapi-diff-workflow-comment
- name: Reply or edit difference comment (changed)
- uses: peter-evans/create-or-update-comment@v2
+ uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
if: ${{ steps.read-diff.outputs.body != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
@@ -112,7 +112,7 @@ jobs:
</details>
- name: Edit difference comment (unchanged)
- uses: peter-evans/create-or-update-comment@v2
+ uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml
index 2578f82cf..f7a77f02b 100644
--- a/.github/workflows/repo-stale.yaml
+++ b/.github/workflows/repo-stale.yaml
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- - uses: actions/stale@v6
+ - uses: actions/stale@5ebf00ea0e4c1561e9b43a292ed34424fb1d4578 # tag=v6
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
days-before-stale: 120
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 8b7e216e4..8db55a6ae 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -22,7 +22,6 @@ using Emby.Drawing;
using Emby.Naming.Common;
using Emby.Notifications;
using Emby.Photos;
-using Emby.Server.Implementations.Archiving;
using Emby.Server.Implementations.Channels;
using Emby.Server.Implementations.Collections;
using Emby.Server.Implementations.Configuration;
@@ -561,8 +560,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IInstallationManager, InstallationManager>();
- serviceCollection.AddSingleton<IZipClient, ZipClient>();
-
serviceCollection.AddSingleton<IServerApplicationHost>(this);
serviceCollection.AddSingleton(ApplicationPaths);
@@ -1091,15 +1088,7 @@ namespace Emby.Server.Implementations
return GetLocalApiUrl(request.Host.Host, request.Scheme, requestPort);
}
- // Published server ends with a /
- if (!string.IsNullOrEmpty(PublishedServerUrl))
- {
- // Published server ends with a '/', so we need to remove it.
- return PublishedServerUrl.Trim('/');
- }
-
- string smart = NetManager.GetBindInterface(request, out var port);
- return GetLocalApiUrl(smart.Trim('/'), request.Scheme, port);
+ return GetSmartApiUrl(request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
}
/// <inheritdoc/>
diff --git a/Emby.Server.Implementations/Archiving/ZipClient.cs b/Emby.Server.Implementations/Archiving/ZipClient.cs
deleted file mode 100644
index 6a3b250d2..000000000
--- a/Emby.Server.Implementations/Archiving/ZipClient.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using System.IO;
-using MediaBrowser.Model.IO;
-using SharpCompress.Common;
-using SharpCompress.Readers;
-using SharpCompress.Readers.GZip;
-
-namespace Emby.Server.Implementations.Archiving
-{
- /// <summary>
- /// Class DotNetZipClient.
- /// </summary>
- public class ZipClient : IZipClient
- {
- /// <inheritdoc />
- public void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles)
- {
- using var reader = GZipReader.Open(source);
- var options = new ExtractionOptions
- {
- ExtractFullPath = true,
- Overwrite = overwriteExistingFiles
- };
-
- Directory.CreateDirectory(targetPath);
- reader.WriteAllToDirectory(targetPath, options);
- }
-
- /// <inheritdoc />
- public void ExtractFirstFileFromGz(Stream source, string targetPath, string defaultFileName)
- {
- using var reader = GZipReader.Open(source);
- if (reader.MoveToNextEntry())
- {
- var entry = reader.Entry;
-
- var filename = entry.Key;
- if (string.IsNullOrWhiteSpace(filename))
- {
- filename = defaultFileName;
- }
-
- reader.WriteEntryToFile(Path.Combine(targetPath, filename));
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 2792a4c7c..ff1102a05 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -25,14 +25,13 @@
<ItemGroup>
<PackageReference Include="DiscUtils.Udf" Version="0.16.13" />
<PackageReference Include="Jellyfin.XmlTv" Version="10.8.0" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" />
- <PackageReference Include="Mono.Nat" Version="3.0.3" />
- <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.4" />
- <PackageReference Include="sharpcompress" Version="0.32.2" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.10" />
+ <PackageReference Include="Mono.Nat" Version="3.0.4" />
+ <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.3.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
</ItemGroup>
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index ee94670eb..cef82ebbc 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -665,11 +665,7 @@ namespace Emby.Server.Implementations.Library
if (result?.Items.Count > 0)
{
var items = result.Items;
- foreach (var item in items)
- {
- ResolverHelper.SetInitialItemValues(item, parent, this, directoryService);
- }
-
+ items.RemoveAll(item => !ResolverHelper.SetInitialItemValues(item, parent, this, directoryService));
items.AddRange(ResolveFileList(result.ExtraFiles, directoryService, parent, collectionType, resolvers, libraryOptions));
return items;
}
diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
index 20a2edb05..609b95772 100644
--- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs
+++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
@@ -45,42 +45,42 @@ namespace Emby.Server.Implementations.Library
.ThenByDescending(x => x.IsForced && string.Equals(x.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
.ThenByDescending(x => x.IsForced)
.ThenByDescending(x => x.IsDefault)
+ .ThenByDescending(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase))
.ToList();
MediaStream? stream = null;
if (mode == SubtitlePlaybackMode.Default)
{
- // Prefer embedded metadata over smart logic
- stream = sortedStreams.FirstOrDefault(s => s.IsExternal || s.IsForced || s.IsDefault);
-
- // if the audio language is not understood by the user, load their preferred subs, if there are any
- if (stream == null && !preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
- {
- stream = sortedStreams.FirstOrDefault(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase));
- }
+ // Load subtitles according to external, forced and default flags.
+ stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
}
else if (mode == SubtitlePlaybackMode.Smart)
{
- // if the audio language is not understood by the user, load their preferred subs, if there are any
+ // Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages.
+ // If no subtitles of preferred language available, use default behaviour.
if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
{
- stream = streams.FirstOrDefault(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase)) ??
- streams.FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase));
+ stream = sortedStreams.FirstOrDefault(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ??
+ sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
+ }
+ else
+ {
+ // Respect forced flag.
+ stream = sortedStreams.FirstOrDefault(x => x.IsForced);
}
}
else if (mode == SubtitlePlaybackMode.Always)
{
- // always load the most suitable full subtitles
- stream = sortedStreams.FirstOrDefault(s => !s.IsForced);
+ // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise default behaviour.
+ stream = sortedStreams.FirstOrDefault(x => !x.IsForced && preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ??
+ sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
}
else if (mode == SubtitlePlaybackMode.OnlyForced)
{
- // always load the most suitable full subtitles
+ // Only load subtitles that are flagged forced.
stream = sortedStreams.FirstOrDefault(x => x.IsForced);
}
- // load forced subs if we have found no suitable full subtitles
- stream ??= sortedStreams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase));
return stream?.Index;
}
diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs
index ac75e5d3a..4100a74a5 100644
--- a/Emby.Server.Implementations/Library/ResolverHelper.cs
+++ b/Emby.Server.Implementations/Library/ResolverHelper.cs
@@ -20,8 +20,9 @@ namespace Emby.Server.Implementations.Library
/// <param name="parent">The parent.</param>
/// <param name="libraryManager">The library manager.</param>
/// <param name="directoryService">The directory service.</param>
+ /// <returns>True if initializing was successful.</returns>
/// <exception cref="ArgumentException">Item must have a path.</exception>
- public static void SetInitialItemValues(BaseItem item, Folder? parent, ILibraryManager libraryManager, IDirectoryService directoryService)
+ public static bool SetInitialItemValues(BaseItem item, Folder? parent, ILibraryManager libraryManager, IDirectoryService directoryService)
{
// This version of the below method has no ItemResolveArgs, so we have to require the path already being set
if (string.IsNullOrEmpty(item.Path))
@@ -44,12 +45,14 @@ namespace Emby.Server.Implementations.Library
var fileInfo = directoryService.GetFile(item.Path);
if (fileInfo == null)
{
- throw new FileNotFoundException("Can't find item path.", item.Path);
+ return false;
}
SetDateCreated(item, fileInfo);
EnsureName(item, fileInfo);
+
+ return true;
}
/// <summary>
diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
index 3d6b9f3b6..b2a7abb1b 100644
--- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -38,7 +38,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>`0.</returns>
- public override T Resolve(ItemResolveArgs args)
+ protected override T Resolve(ItemResolveArgs args)
{
return ResolveVideo<T>(args, false);
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index 8f224f547..6fc200e3b 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
@@ -8,15 +8,16 @@ using System.Linq;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
namespace Emby.Server.Implementations.Library.Resolvers.Books
{
- public class BookResolver : MediaBrowser.Controller.Resolvers.ItemResolver<Book>
+ public class BookResolver : ItemResolver<Book>
{
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" };
- public override Book Resolve(ItemResolveArgs args)
+ protected override Book Resolve(ItemResolveArgs args)
{
var collectionType = args.GetCollectionType();
diff --git a/Emby.Server.Implementations/Library/Resolvers/GenericFolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/GenericFolderResolver.cs
index f109a5e9a..079962282 100644
--- a/Emby.Server.Implementations/Library/Resolvers/GenericFolderResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/GenericFolderResolver.cs
@@ -2,6 +2,7 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Resolvers;
namespace Emby.Server.Implementations.Library.Resolvers
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
deleted file mode 100644
index 3f29ab191..000000000
--- a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-#nullable disable
-
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Resolvers;
-
-namespace Emby.Server.Implementations.Library.Resolvers
-{
- /// <summary>
- /// Class ItemResolver.
- /// </summary>
- /// <typeparam name="T">The type of BaseItem.</typeparam>
- public abstract class ItemResolver<T> : IItemResolver
- where T : BaseItem, new()
- {
- /// <summary>
- /// Gets the priority.
- /// </summary>
- /// <value>The priority.</value>
- public virtual ResolverPriority Priority => ResolverPriority.First;
-
- /// <summary>
- /// Resolves the specified args.
- /// </summary>
- /// <param name="args">The args.</param>
- /// <returns>`0.</returns>
- protected virtual T Resolve(ItemResolveArgs args)
- {
- return null;
- }
-
- /// <summary>
- /// Sets initial values on the newly resolved item.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <param name="args">The args.</param>
- protected virtual void SetInitialItemValues(T item, ItemResolveArgs args)
- {
- }
-
- /// <summary>
- /// Resolves the path.
- /// </summary>
- /// <param name="args">The args.</param>
- /// <returns>BaseItem.</returns>
- BaseItem IItemResolver.ResolvePath(ItemResolveArgs args)
- {
- var item = Resolve(args);
-
- if (item != null)
- {
- SetInitialItemValues(item, args);
- }
-
- return item;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index b2f388a66..8f9e5f01b 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// </summary>
/// <param name="args">The args.</param>
/// <returns>Video.</returns>
- public override Video Resolve(ItemResolveArgs args)
+ protected override Video Resolve(ItemResolveArgs args)
{
var collectionType = args.GetCollectionType();
diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
index af4abfb80..e11fb262e 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
@@ -12,6 +12,7 @@ using Jellyfin.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
namespace Emby.Server.Implementations.Library.Resolvers
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
index bfa73af2f..9ba079edf 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
@@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// </summary>
/// <param name="args">The args.</param>
/// <returns>Episode.</returns>
- public override Episode Resolve(ItemResolveArgs args)
+ protected override Episode Resolve(ItemResolveArgs args)
{
var parent = args.Parent;
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 4da677636..74321a256 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -2219,6 +2219,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
continue;
}
+ // Skip ShowId without SubKey from duplicate removal actions - https://github.com/jellyfin/jellyfin/issues/5856
+ if (group.Key.EndsWith("0000", StringComparison.Ordinal))
+ {
+ continue;
+ }
+
HandleDuplicateShowIds(groupTimers);
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
index bd1cd1e1d..7570a2bcf 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
@@ -6,9 +6,9 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
+using System.IO.Compression;
using System.Linq;
using System.Net.Http;
-using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Extensions;
@@ -33,20 +33,17 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<XmlTvListingsProvider> _logger;
private readonly IFileSystem _fileSystem;
- private readonly IZipClient _zipClient;
public XmlTvListingsProvider(
IServerConfigurationManager config,
IHttpClientFactory httpClientFactory,
ILogger<XmlTvListingsProvider> logger,
- IFileSystem fileSystem,
- IZipClient zipClient)
+ IFileSystem fileSystem)
{
_config = config;
_httpClientFactory = httpClientFactory;
_logger = logger;
_fileSystem = fileSystem;
- _zipClient = zipClient;
}
public string Name => "XmlTV";
@@ -67,16 +64,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
_logger.LogInformation("xmltv path: {Path}", info.Path);
- if (!info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
- {
- return UnzipIfNeeded(info.Path, info.Path);
- }
-
string cacheFilename = info.Id + ".xml";
string cacheFile = Path.Combine(_config.ApplicationPaths.CachePath, "xmltv", cacheFilename);
+
if (File.Exists(cacheFile) && File.GetLastWriteTimeUtc(cacheFile) >= DateTime.UtcNow.Subtract(_maxCacheAge))
{
- return UnzipIfNeeded(info.Path, cacheFile);
+ return cacheFile;
}
// Must check if file exists as parent directory may not exist.
@@ -84,93 +77,48 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
File.Delete(cacheFile);
}
+ else
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(cacheFile));
+ }
- _logger.LogInformation("Downloading xmltv listings from {Path}", info.Path);
-
- Directory.CreateDirectory(Path.GetDirectoryName(cacheFile));
+ if (info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogInformation("Downloading xmltv listings from {Path}", info.Path);
- using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(info.Path, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, FileOptions.Asynchronous))
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(info.Path, cancellationToken).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(false);
+ }
+ else
{
- await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
+ await using var stream = AsyncFile.OpenRead(info.Path);
+ return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(false);
}
-
- return UnzipIfNeeded(info.Path, cacheFile);
}
- private string UnzipIfNeeded(ReadOnlySpan<char> originalUrl, string file)
+ private async Task<string> UnzipIfNeededAndCopy(string originalUrl, Stream stream, string file, CancellationToken cancellationToken)
{
- ReadOnlySpan<char> ext = Path.GetExtension(originalUrl.LeftPart('?'));
+ await using var fileStream = new FileStream(file, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
- if (ext.Equals(".gz", StringComparison.OrdinalIgnoreCase))
+ if (Path.GetExtension(originalUrl.AsSpan().LeftPart('?')).Equals(".gz", StringComparison.OrdinalIgnoreCase))
{
try
{
- string tempFolder = ExtractGz(file);
- return FindXmlFile(tempFolder);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error extracting from gz file {File}", file);
- }
-
- try
- {
- string tempFolder = ExtractFirstFileFromGz(file);
- return FindXmlFile(tempFolder);
+ using var reader = new GZipStream(stream, CompressionMode.Decompress);
+ await reader.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error extracting from zip file {File}", file);
+ _logger.LogError(ex, "Error extracting from gz file {File}", originalUrl);
}
}
-
- return file;
- }
-
- private string ExtractFirstFileFromGz(string file)
- {
- using (var stream = File.OpenRead(file))
- {
- string tempFolder = GetTempFolderPath(stream);
- Directory.CreateDirectory(tempFolder);
-
- _zipClient.ExtractFirstFileFromGz(stream, tempFolder, "data.xml");
-
- return tempFolder;
- }
- }
-
- private string ExtractGz(string file)
- {
- using (var stream = File.OpenRead(file))
+ else
{
- string tempFolder = GetTempFolderPath(stream);
- Directory.CreateDirectory(tempFolder);
-
- _zipClient.ExtractAllFromGz(stream, tempFolder, true);
-
- return tempFolder;
+ await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
- }
- private string GetTempFolderPath(Stream stream)
- {
-#pragma warning disable CA5351
- using var md5 = MD5.Create();
-#pragma warning restore CA5351
- var checksum = Convert.ToHexString(md5.ComputeHash(stream));
- stream.Position = 0;
- return Path.Combine(_config.ApplicationPaths.TempDirectory, checksum);
- }
-
- private string FindXmlFile(string directory)
- {
- return _fileSystem.GetFiles(directory, true)
- .Where(i => string.Equals(i.Extension, ".xml", StringComparison.OrdinalIgnoreCase))
- .Select(i => i.FullName)
- .FirstOrDefault();
+ return file;
}
public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
@@ -213,16 +161,16 @@ namespace Emby.Server.Implementations.LiveTv.Listings
IsMovie = program.Categories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
IsNews = program.Categories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
IsSports = program.Categories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
- ImageUrl = program.Icon != null && !string.IsNullOrEmpty(program.Icon.Source) ? program.Icon.Source : null,
- HasImage = program.Icon != null && !string.IsNullOrEmpty(program.Icon.Source),
- OfficialRating = program.Rating != null && !string.IsNullOrEmpty(program.Rating.Value) ? program.Rating.Value : null,
+ ImageUrl = string.IsNullOrEmpty(program.Icon?.Source) ? null : program.Icon.Source,
+ HasImage = !string.IsNullOrEmpty(program.Icon?.Source),
+ OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value,
CommunityRating = program.StarRating,
SeriesId = program.Episode == null ? null : program.Title.GetMD5().ToString("N", CultureInfo.InvariantCulture)
};
if (string.IsNullOrWhiteSpace(program.ProgramId))
{
- string uniqueString = (program.Title ?? string.Empty) + (episodeTitle ?? string.Empty) /*+ (p.IceTvEpisodeNumber ?? string.Empty)*/;
+ string uniqueString = (program.Title ?? string.Empty) + (episodeTitle ?? string.Empty);
if (programInfo.SeasonNumber.HasValue)
{
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index 9dc2fe799..ada3c7730 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -97,7 +97,7 @@
"TasksChannelsCategory": "قنوات الإنترنت",
"TasksLibraryCategory": "مكتبة",
"TasksMaintenanceCategory": "صيانة",
- "TaskRefreshLibraryDescription": "يفصح مكتبة الوسائط الخاصة بك بحثًا عن ملفات جديدة، ومن ثم يتحدث البيانات الوصفية.",
+ "TaskRefreshLibraryDescription": "يفحص مكتبة الوسائط الخاصة بك باحثا عن ملفات جديدة، ومن ثم يتحدث البيانات الوصفية.",
"TaskRefreshLibrary": "افحص مكتبة الوسائط",
"TaskRefreshChapterImagesDescription": "يُنشئ صور مصغرة لمقاطع الفيديو التي تحتوي على فصول.",
"TaskRefreshChapterImages": "استخراج صور الفصل",
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index 943fc651f..08db5a30e 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Optimalizovat databázi",
"TaskKeyframeExtractorDescription": "Vytahuje klíčové snímky ze souborů videa za účelem vytváření přesnějších seznamů přehrávání HLS. Tento úkol může trvat velmi dlouho.",
"TaskKeyframeExtractor": "Vytahovač klíčových snímků",
- "External": "Externí"
+ "External": "Externí",
+ "HearingImpaired": "Sluchově postižení"
}
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index 9c278db4d..e1c3e9de1 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Datenbank optimieren",
"TaskKeyframeExtractorDescription": "Extrahiere Keyframes aus Videodateien, um präzisere HLS-Playlisten zu erzeugen. Dieser Vorgang kann sehr lange dauern.",
"TaskKeyframeExtractor": "Keyframe Extraktor",
- "External": "Extern"
+ "External": "Extern",
+ "HearingImpaired": "Hörgeschädigt"
}
diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json
index 9e216a166..8e9287af4 100644
--- a/Emby.Server.Implementations/Localization/Core/el.json
+++ b/Emby.Server.Implementations/Localization/Core/el.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Βελτιστοποίηση βάσης δεδομένων",
"TaskKeyframeExtractorDescription": "Εξάγει καρέ από αρχεία βίντεο για να δημιουργήσει πιο ακριβείς λίστες αναπαραγωγής HLS. Αυτή η διεργασία μπορεί να πάρει χρόνο.",
"TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο",
- "External": "Εξωτερικό"
+ "External": "Εξωτερικό",
+ "HearingImpaired": "Με προβλήματα ακοής"
}
diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json
index 862410c54..243688388 100644
--- a/Emby.Server.Implementations/Localization/Core/en-GB.json
+++ b/Emby.Server.Implementations/Localization/Core/en-GB.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Optimise database",
"TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.",
"TaskKeyframeExtractor": "Keyframe Extractor",
- "External": "External"
+ "External": "External",
+ "HearingImpaired": "Hearing Impaired"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json
index 1289172ba..8ad9e8c71 100644
--- a/Emby.Server.Implementations/Localization/Core/es-AR.json
+++ b/Emby.Server.Implementations/Localization/Core/es-AR.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Optimización de base de datos",
"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"
+ "TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
+ "HearingImpaired": "Personas con discapacidad auditiva"
}
diff --git a/Emby.Server.Implementations/Localization/Core/eu.json b/Emby.Server.Implementations/Localization/Core/eu.json
index dfedce7b3..d657ac7b6 100644
--- a/Emby.Server.Implementations/Localization/Core/eu.json
+++ b/Emby.Server.Implementations/Localization/Core/eu.json
@@ -116,5 +116,12 @@
"CameraImageUploadedFrom": "{0}-tik kamera irudi berri bat igo da",
"AuthenticationSucceededWithUserName": "{0} ongi autentifikatu da",
"Application": "Aplikazioa",
- "AppDeviceValues": "App: {0}, Gailua: {1}"
+ "AppDeviceValues": "App: {0}, Gailua: {1}",
+ "HearingImpaired": "Entzunaldia aldatua",
+ "ProviderValue": "Hornitzailea: {0}",
+ "TaskKeyframeExtractorDescription": "Bideo fitxategietako fotograma gakoak ateratzen ditu HLS erreprodukzio-zerrenda zehatzagoak sortzeko. Zeregin honek denbora asko iraun dezake.",
+ "HeaderRecordingGroups": "Grabaketa taldeak",
+ "Inherit": "Oinordetu",
+ "TaskOptimizeDatabaseDescription": "Datu-basea trinkotu eta bertatik espazioa askatzen du. Liburutegia eskaneatu ondoren edo datu-basean aldaketak egin ondoren ataza hau exekutatzeak errendimendua hobetu lezake.",
+ "TaskKeyframeExtractor": "Fotograma gakoen erauzgailua"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json
index f0cafd1c0..ec72d58dd 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/Emby.Server.Implementations/Localization/Core/fi.json
@@ -122,5 +122,6 @@
"TaskOptimizeDatabase": "Optimoi tietokanta",
"TaskKeyframeExtractorDescription": "Purkaa videotiedostojen avainkuvat tarkempien HLS-toistolistojen luomiseksi. Tehtävä saattaa kestää huomattavan pitkään.",
"TaskKeyframeExtractor": "Avainkuvien purkain",
- "External": "Ulkoinen"
+ "External": "Ulkoinen",
+ "HearingImpaired": "Kuulorajoitteinen"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json
index 24ca8f861..3ee045d89 100644
--- a/Emby.Server.Implementations/Localization/Core/fr-CA.json
+++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json
@@ -5,7 +5,7 @@
"Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
"Books": "Livres",
- "CameraImageUploadedFrom": "Une nouvelle image de caméra a été téléchargée depuis {0}",
+ "CameraImageUploadedFrom": "Une nouvelle photo a été téléversée depuis {0}",
"Channels": "Chaînes",
"ChapterNameValue": "Chapitre {0}",
"Collections": "Collections",
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Optimiser la base de données",
"TaskKeyframeExtractorDescription": "Extrait les images clés des fichiers vidéo pour créer des listes de lecture HLS plus précises. Cette tâche peut durer très longtemps.",
"TaskKeyframeExtractor": "Extracteur d'image clé",
- "External": "Externe"
+ "External": "Externe",
+ "HearingImpaired": "Malentendants"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index 648c878e9..768245a09 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Optimiser la base de données",
"TaskKeyframeExtractorDescription": "Extrait les images clés des fichiers vidéo pour créer des listes de lecture HLS plus précises. Cette tâche peut durer très longtemps.",
"TaskKeyframeExtractor": "Extracteur d'image clé",
- "External": "Externe"
+ "External": "Externe",
+ "HearingImpaired": "Malentendants"
}
diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json
index b433c6f68..76a98aa54 100644
--- a/Emby.Server.Implementations/Localization/Core/gl.json
+++ b/Emby.Server.Implementations/Localization/Core/gl.json
@@ -47,7 +47,7 @@
"HeaderFavoriteEpisodes": "Episodios Favoritos",
"HeaderFavoriteArtists": "Artistas Favoritos",
"HeaderFavoriteAlbums": "Álbunes Favoritos",
- "HeaderContinueWatching": "Seguir mirando",
+ "HeaderContinueWatching": "Seguir vendo",
"HeaderAlbumArtists": "Artistas do Album",
"Genres": "Xéneros",
"Forced": "Forzado",
@@ -119,5 +119,9 @@
"UserOnlineFromDevice": "{0} está en liña desde {1}",
"UserOfflineFromDevice": "{0} desconectouse desde {1}",
"TaskOptimizeDatabaseDescription": "Compacta e libera o espazo libre da base de datos. Executar esta tarefa logo de realizar mudanzas que impliquen modificacións da base de datos ou despois de escanear a biblioteca pode traer mellorías de desempeño.",
- "TaskOptimizeDatabase": "Optimizar base de datos"
+ "TaskOptimizeDatabase": "Optimizar base de datos",
+ "TaskKeyframeExtractorDescription": "Extrae fragmentos do vídeo para crear listas de reprodución HLS máis precisas. Podería levarlle bastante tempo.",
+ "External": "Externo",
+ "HearingImpaired": "Problemas de audición",
+ "TaskKeyframeExtractor": "Extractor de fragmentos"
}
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index c635dab23..694a3d688 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabaseDescription": "דוחס את מסד הנתונים ומוריד את שטח האחסון שבשימוש. הרצה של פעולה זו לאחר סריקת הספרייה או שינויים אחרים שמשפיעים על מסד הנתונים יכולה לשפר ביצועים.",
"TaskKeyframeExtractorDescription": "חלץ תמונות מפתח מקבצי וידאו בכדי ליצור רשימות השמעה מדויקות יותר של HLS. משימה זו עלולה להימשך זמן רב.",
"TaskKeyframeExtractor": "מחלץ תמונות מפתח",
- "External": "חיצוני"
+ "External": "חיצוני",
+ "HearingImpaired": "לקוי שמיעה"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json
index c7f2f9c85..62d48cebd 100644
--- a/Emby.Server.Implementations/Localization/Core/hu.json
+++ b/Emby.Server.Implementations/Localization/Core/hu.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Adatbázis optimalizálása",
"TaskKeyframeExtractor": "Kulcskockák kibontása",
"TaskKeyframeExtractorDescription": "Kulcskockákat bont ki a videofájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat.",
- "External": "Külső"
+ "External": "Külső",
+ "HearingImpaired": "Hallássérült"
}
diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json
index 3e05525c8..695c0f404 100644
--- a/Emby.Server.Implementations/Localization/Core/id.json
+++ b/Emby.Server.Implementations/Localization/Core/id.json
@@ -122,5 +122,6 @@
"TaskOptimizeDatabase": "Optimalkan basis data",
"TaskKeyframeExtractorDescription": "Ekstrak bingkai utama dari file video untuk membuat daftar putar HLS yang lebih tepat. Tugas ini dapat berjalan untuk waktu yang lama.",
"TaskKeyframeExtractor": "Ekstraktor Bingkai Utama",
- "External": "Luar"
+ "External": "Luar",
+ "HearingImpaired": "Gangguan Pendengaran"
}
diff --git a/Emby.Server.Implementations/Localization/Core/jbo.json b/Emby.Server.Implementations/Localization/Core/jbo.json
new file mode 100644
index 000000000..1b47bb2f2
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/jbo.json
@@ -0,0 +1,7 @@
+{
+ "Albums": "lo albuma",
+ "Artists": "lo larpra",
+ "Books": "lo cukta",
+ "HeaderAlbumArtists": "lo albuma larpra",
+ "Playlists": "lo zgipor"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/km.json b/Emby.Server.Implementations/Localization/Core/km.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/km.json
@@ -0,0 +1 @@
+{}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index 3f22355d6..d7b2bc00c 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Database optimaliseren",
"TaskKeyframeExtractorDescription": "Haalt keyframes uit videobestanden om preciezere HLS afspeellijsten te maken. Dit kan lang duren.",
"TaskKeyframeExtractor": "Keyframe Extractor",
- "External": "Extern"
+ "External": "Extern",
+ "HearingImpaired": "Slechthorend"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json
index 38a36a7e0..b9b93b7b6 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-BR.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Otimizar base de dados",
"TaskKeyframeExtractor": "Extrator de quadro-chave",
"TaskKeyframeExtractorDescription": "Extrai quadros-chave de arquivos de vídeo para criar listas de reprodução HLS mais precisas. Esta tarefa pode ser executada por um longo tempo.",
- "External": "Externo"
+ "External": "Externo",
+ "HearingImpaired": "Deficiência Auditiva"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json
index 2766dab06..d1b73a3eb 100644
--- a/Emby.Server.Implementations/Localization/Core/sq.json
+++ b/Emby.Server.Implementations/Localization/Core/sq.json
@@ -119,5 +119,9 @@
"Forced": "I detyruar",
"Default": "Parazgjedhur",
"TaskOptimizeDatabaseDescription": "Kompakton bazën e të dhënave dhe shkurton hapësirën e lirë. Drejtimi i kësaj detyre pasi skanoni bibliotekën ose bëni ndryshime të tjera që nënkuptojnë modifikime të bazës së të dhënave mund të përmirësojë performancën.",
- "TaskOptimizeDatabase": "Optimizo databazën"
+ "TaskOptimizeDatabase": "Optimizo databazën",
+ "TaskKeyframeExtractorDescription": "Nxjerrë kornizat kryesore nga skedarët video për të krijuar lista luajtjeje më të sakta HLS. Ky veprim mund të dojë një kohë të gjatë për tu kompletuar.",
+ "TaskKeyframeExtractor": "Nxjerrës i kornizës kryesore",
+ "External": "Jashtem",
+ "HearingImpaired": "Dëgjimi i dëmtuar"
}
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
index 3e0fd11c8..92ce616f2 100644
--- a/Emby.Server.Implementations/Localization/Core/uk.json
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -122,5 +122,6 @@
"TaskOptimizeDatabaseDescription": "Стискає базу даних та збільшує вільний простір. Виконання цього завдання після сканування медіатеки або внесення інших змін, які передбачають модифікацію бази даних може покращити продуктивність.",
"TaskKeyframeExtractorDescription": "Витягує ключові кадри з відеофайлів для створення більш точних списків відтворення HLS. Це завдання може виконуватися протягом тривалого часу.",
"TaskKeyframeExtractor": "Екстрактор ключових кадрів",
- "External": "Зовнішній"
+ "External": "Зовнішній",
+ "HearingImpaired": "З порушеннями слуху"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index a121fc376..ccfbeef0c 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "优化数据库",
"TaskKeyframeExtractorDescription": "从视频文件中提取关键帧以创建更准确的HLS播放列表。这项任务可能需要很长时间。",
"TaskKeyframeExtractor": "关键帧提取器",
- "External": "外部"
+ "External": "外部",
+ "HearingImpaired": "听力障碍"
}
diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
index 0dd4bf803..3a2ba033e 100644
--- a/Jellyfin.Api/Controllers/PlaystateController.cs
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -274,7 +274,7 @@ namespace Jellyfin.Api.Controllers
};
playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
- playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);;
+ playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false);
return NoContent();
}
@@ -319,7 +319,7 @@ namespace Jellyfin.Api.Controllers
await _transcodingJobHelper.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
}
- playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);;
+ playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false);
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index 28415555e..31b95162d 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -182,7 +182,7 @@ namespace Jellyfin.Api.Controllers
};
await _sessionManager.SendPlayCommand(
- await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false),
+ await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false),
sessionId,
playRequest,
CancellationToken.None)
@@ -210,7 +210,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? controllingUserId)
{
await _sessionManager.SendPlaystateCommand(
- await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false),
+ await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false),
sessionId,
new PlaystateRequest()
{
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index 01e13b4fe..d77126a35 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -108,7 +108,7 @@ namespace Jellyfin.Api.Controllers
{
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
- if (!userId.HasValue || userId.Value.Equals(Guid.Empty))
+ if (!userId.HasValue || userId.Value.Equals(default))
{
userId = User.GetUserId();
}
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 7e64cf645..595c627f8 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -17,7 +17,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.9" />
+ <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.10" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" />
diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index cbbeee024..89de998ca 100644
--- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
+++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
@@ -18,8 +18,8 @@
<ItemGroup>
<PackageReference Include="BlurHashSharp" Version="1.2.0" />
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
- <PackageReference Include="SkiaSharp" Version="2.88.2" />
- <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.2" />
+ <PackageReference Include="SkiaSharp" Version="2.88.3" />
+ <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" />
<PackageReference Include="SkiaSharp.Svg" Version="1.60.0" />
</ItemGroup>
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index 83b226278..e1f902efc 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -27,13 +27,13 @@
<ItemGroup>
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.10" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.10" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.9">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index b2d79050b..a5f20d671 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -37,8 +37,8 @@
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.9" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.9" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.10" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.10" />
<PackageReference Include="prometheus-net" Version="6.0.0" />
<PackageReference Include="prometheus-net.AspNetCore" Version="6.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 41fce67fa..24163f1df 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -775,36 +775,6 @@ namespace MediaBrowser.Controller.Entities
return Id.ToString("N", CultureInfo.InvariantCulture);
}
- private List<Tuple<StringBuilder, bool>> GetSortChunks(string s1)
- {
- var list = new List<Tuple<StringBuilder, bool>>();
-
- int thisMarker = 0;
-
- while (thisMarker < s1.Length)
- {
- char thisCh = s1[thisMarker];
-
- var thisChunk = new StringBuilder();
- bool isNumeric = char.IsDigit(thisCh);
-
- while (thisMarker < s1.Length && char.IsDigit(thisCh) == isNumeric)
- {
- thisChunk.Append(thisCh);
- thisMarker++;
-
- if (thisMarker < s1.Length)
- {
- thisCh = s1[thisMarker];
- }
- }
-
- list.Add(new Tuple<StringBuilder, bool>(thisChunk, isNumeric));
- }
-
- return list;
- }
-
public virtual bool CanDelete()
{
if (SourceType == SourceType.Channel)
@@ -951,28 +921,40 @@ namespace MediaBrowser.Controller.Entities
return ModifySortChunks(sortable);
}
- private string ModifySortChunks(string name)
+ internal static string ModifySortChunks(string name)
{
- var chunks = GetSortChunks(name);
+ void AppendChunk(StringBuilder builder, bool isDigitChunk, ReadOnlySpan<char> chunk)
+ {
+ if (isDigitChunk && chunk.Length < 10)
+ {
+ builder.Append('0', 10 - chunk.Length);
+ }
- var builder = new StringBuilder();
+ builder.Append(chunk);
+ }
- foreach (var chunk in chunks)
+ if (name.Length == 0)
{
- var chunkBuilder = chunk.Item1;
+ return string.Empty;
+ }
+
+ var builder = new StringBuilder(name.Length);
- // This chunk is numeric
- if (chunk.Item2)
+ int chunkStart = 0;
+ bool isDigitChunk = char.IsDigit(name[0]);
+ for (int i = 0; i < name.Length; i++)
+ {
+ var isDigit = char.IsDigit(name[i]);
+ if (isDigit != isDigitChunk)
{
- while (chunkBuilder.Length < 10)
- {
- chunkBuilder.Insert(0, '0');
- }
+ AppendChunk(builder, isDigitChunk, name.AsSpan(chunkStart, i - chunkStart));
+ chunkStart = i;
+ isDigitChunk = isDigit;
}
-
- builder.Append(chunkBuilder);
}
+ AppendChunk(builder, isDigitChunk, name.AsSpan(chunkStart));
+
// logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
return builder.ToString().RemoveDiacritics();
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index d0362b128..235a86138 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -31,10 +31,13 @@ namespace MediaBrowser.Controller.MediaEncoding
private const string VideotoolboxAlias = "vt";
private const string OpenclAlias = "ocl";
private const string CudaAlias = "cu";
+ private const string DrmAlias = "dr";
+ private const string VulkanAlias = "vk";
private readonly IApplicationPaths _appPaths;
private readonly IMediaEncoder _mediaEncoder;
private readonly ISubtitleEncoder _subtitleEncoder;
private readonly IConfiguration _config;
+ private readonly Version _minKernelVersionAmdVkFmtModifier = new Version(5, 15);
private readonly Version _minKernelVersioni915Hang = new Version(5, 18);
private static readonly string[] _videoProfilesH264 = new[]
@@ -149,6 +152,14 @@ namespace MediaBrowser.Controller.MediaEncoding
&& _mediaEncoder.SupportsFilter("hwupload_cuda");
}
+ private bool IsVulkanFullSupported()
+ {
+ return _mediaEncoder.SupportsHwaccel("vulkan")
+ && _mediaEncoder.SupportsFilter("libplacebo")
+ && _mediaEncoder.SupportsFilter("scale_vulkan")
+ && _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayVulkanFrameSync);
+ }
+
private bool IsHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
{
if (state.VideoStream == null
@@ -176,6 +187,19 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals(state.VideoStream.VideoRangeType, "HLG", StringComparison.OrdinalIgnoreCase));
}
+ private bool IsVulkanHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
+ {
+ if (state.VideoStream == null)
+ {
+ return false;
+ }
+
+ // libplacebo has partial Dolby Vision to SDR tonemapping support.
+ return options.EnableTonemapping
+ && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
+ && GetVideoColorBitDepth(state) == 10;
+ }
+
private bool IsVaapiVppTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
{
if (state.VideoStream == null
@@ -756,8 +780,13 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (_mediaEncoder.IsVaapiDeviceAmd)
{
- args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias));
- filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
+ if (!IsVulkanFullSupported()
+ || !_mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier
+ || Environment.OSVersion.Version < _minKernelVersionAmdVkFmtModifier)
+ {
+ args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias));
+ filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
+ }
}
else
{
@@ -2774,22 +2803,41 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Empty;
}
- var args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709";
+ var args = string.Empty;
+ var algorithm = options.TonemappingAlgorithm;
- if (hwTonemapSuffix.Contains("vaapi", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(hwTonemapSuffix, "vaapi", StringComparison.OrdinalIgnoreCase))
{
- args += ",procamp_vaapi=b={2}:c={3}:extra_hw_frames=16";
+ args = "tonemap_vaapi=format={0}:p=bt709:t=bt709:m=bt709,procamp_vaapi=b={1}:c={2}:extra_hw_frames=16";
return string.Format(
CultureInfo.InvariantCulture,
args,
- hwTonemapSuffix,
videoFormat ?? "nv12",
options.VppTonemappingBrightness,
options.VppTonemappingContrast);
}
+ else if (string.Equals(hwTonemapSuffix, "vulkan", StringComparison.OrdinalIgnoreCase))
+ {
+ args = "libplacebo=format={1}:tonemapping={2}:color_primaries=bt709:color_trc=bt709:colorspace=bt709:peak_detect=0:upscaler=none:downscaler=none";
+
+ if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase))
+ {
+ args += ":range={6}";
+ }
+
+ if (string.Equals(options.TonemappingAlgorithm, "bt2390", StringComparison.OrdinalIgnoreCase))
+ {
+ algorithm = "bt.2390";
+ }
+
+ else if (string.Equals(options.TonemappingAlgorithm, "none", StringComparison.OrdinalIgnoreCase))
+ {
+ algorithm = "clip";
+ }
+ }
else
{
- args += ":tonemap={2}:peak={3}:desat={4}";
+ args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709:tonemap={2}:peak={3}:desat={4}";
if (options.TonemappingParam != 0)
{
@@ -2807,7 +2855,7 @@ namespace MediaBrowser.Controller.MediaEncoding
args,
hwTonemapSuffix,
videoFormat ?? "nv12",
- options.TonemappingAlgorithm,
+ algorithm,
options.TonemappingPeak,
options.TonemappingDesat,
options.TonemappingParam,
@@ -3770,7 +3818,9 @@ namespace MediaBrowser.Controller.MediaEncoding
var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty;
var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
var isSwEncoder = !vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
- var isVaapiOclSupported = isLinux && IsVaapiSupported(state) && IsVaapiFullSupported() && IsOpenclFullSupported();
+ var isVaapiFullSupported = isLinux && IsVaapiSupported(state) && IsVaapiFullSupported();
+ var isVaapiOclSupported = isVaapiFullSupported && IsOpenclFullSupported();
+ var isVaapiVkSupported = isVaapiFullSupported && IsVulkanFullSupported();
// legacy vaapi pipeline(copy-back)
if ((isSwDecoder && isSwEncoder)
@@ -3798,14 +3848,24 @@ namespace MediaBrowser.Controller.MediaEncoding
if (_mediaEncoder.IsVaapiDeviceInteliHD)
{
// Intel iHD path, with extra vpp tonemap and overlay support.
- return GetVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
+ return GetIntelVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
+ }
+
+ // prefered vaapi + vulkan filters pipeline
+ if (_mediaEncoder.IsVaapiDeviceAmd
+ && isVaapiVkSupported
+ && _mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier
+ && Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
+ {
+ // AMD radeonsi path(Vega/gfx9+, kernel>=5.15), with extra vulkan tonemap and overlay support.
+ return GetAmdVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
}
- // Intel i965 and Amd radeonsi/r600 path, only featuring scale and deinterlace support.
+ // Intel i965 and Amd radeonsi/r600 path(Polaris/gfx8-), only featuring scale and deinterlace support.
return GetVaapiLimitedVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
}
- public (List<string> MainFilters, List<string> SubFilters, List<string> OverlayFilters) GetVaapiFullVidFiltersPrefered(
+ public (List<string> MainFilters, List<string> SubFilters, List<string> OverlayFilters) GetIntelVaapiFullVidFiltersPrefered(
EncodingJobInfo state,
EncodingOptions options,
string vidDecoder,
@@ -4003,6 +4063,203 @@ namespace MediaBrowser.Controller.MediaEncoding
return (mainFilters, subFilters, overlayFilters);
}
+ public (List<string> MainFilters, List<string> SubFilters, List<string> OverlayFilters) GetAmdVaapiFullVidFiltersPrefered(
+ EncodingJobInfo state,
+ EncodingOptions options,
+ string vidDecoder,
+ string vidEncoder)
+ {
+ var inW = state.VideoStream?.Width;
+ var inH = state.VideoStream?.Height;
+ var reqW = state.BaseRequest.Width;
+ var reqH = state.BaseRequest.Height;
+ var reqMaxW = state.BaseRequest.MaxWidth;
+ var reqMaxH = state.BaseRequest.MaxHeight;
+ var threeDFormat = state.MediaSource.Video3DFormat;
+
+ var isVaapiDecoder = vidDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
+ var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
+ var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
+ var isSwEncoder = !isVaapiEncoder;
+ var isVaInVaOut = isVaapiDecoder && isVaapiEncoder;
+
+ var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
+ var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
+ var doVkTonemap = IsVulkanHwTonemapAvailable(state, options);
+ var doDeintH2645 = doDeintH264 || doDeintHevc;
+
+ var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+ var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
+ var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream;
+ var hasAssSubs = hasSubs
+ && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase));
+
+ /* Make main filters for video stream */
+ var mainFilters = new List<string>();
+
+ mainFilters.Add(GetOverwriteColorPropertiesParam(state, doVkTonemap));
+
+ if (isSwDecoder)
+ {
+ // INPUT sw surface(memory)
+ // sw deint
+ if (doDeintH2645)
+ {
+ var swDeintFilter = GetSwDeinterlaceFilter(state, options);
+ mainFilters.Add(swDeintFilter);
+ }
+
+ var outFormat = doVkTonemap ? "yuv420p10le" : "nv12";
+ var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
+ // sw scale
+ mainFilters.Add(swScaleFilter);
+ mainFilters.Add("format=" + outFormat);
+
+ // keep video at memory except vk tonemap,
+ // since the overhead caused by hwupload >>> using sw filter.
+ // sw => hw
+ if (doVkTonemap)
+ {
+ mainFilters.Add("hwupload=derive_device=vulkan:extra_hw_frames=16");
+ }
+ }
+ else if (isVaapiDecoder)
+ {
+ // INPUT vaapi surface(vram)
+ // hw deint
+ if (doDeintH2645)
+ {
+ var deintFilter = GetHwDeinterlaceFilter(state, options, "vaapi");
+ mainFilters.Add(deintFilter);
+ }
+
+ var outFormat = doVkTonemap ? string.Empty : (hasSubs && isVaInVaOut ? "bgra" : "nv12");
+ var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+
+ // allocate extra pool sizes for overlay_vulkan
+ if (!string.IsNullOrEmpty(hwScaleFilter) && isVaInVaOut && hasSubs)
+ {
+ hwScaleFilter += ":extra_hw_frames=32";
+ }
+
+ // hw scale
+ mainFilters.Add(hwScaleFilter);
+ }
+
+ if ((isVaapiDecoder && doVkTonemap) || (isVaInVaOut && (doVkTonemap || hasSubs)))
+ {
+ // map from vaapi to vulkan via vaapi-vulkan interop (Vega/gfx9+).
+ mainFilters.Add("hwmap=derive_device=vulkan");
+ }
+
+ // vk tonemap
+ if (doVkTonemap)
+ {
+ var outFormat = isVaInVaOut && hasSubs ? "bgra" : "nv12";
+ var tonemapFilter = GetHwTonemapFilter(options, "vulkan", outFormat);
+ mainFilters.Add(tonemapFilter);
+ }
+
+ if (doVkTonemap && isVaInVaOut && !hasSubs)
+ {
+ // OUTPUT vaapi(nv12/bgra) surface(vram)
+ // reverse-mapping via vaapi-vulkan interop.
+ mainFilters.Add("hwmap=derive_device=vaapi:reverse=1");
+ mainFilters.Add("format=vaapi");
+ }
+
+ var memoryOutput = false;
+ var isUploadForVkTonemap = isSwDecoder && doVkTonemap;
+ if ((isVaapiDecoder && isSwEncoder) || isUploadForVkTonemap)
+ {
+ memoryOutput = true;
+
+ // OUTPUT nv12 surface(memory)
+ mainFilters.Add("hwdownload");
+ mainFilters.Add("format=nv12");
+ }
+
+ // OUTPUT nv12 surface(memory)
+ if (isSwDecoder && isVaapiEncoder)
+ {
+ memoryOutput = true;
+ }
+
+ if (memoryOutput)
+ {
+ // text subtitles
+ if (hasTextSubs)
+ {
+ var textSubtitlesFilter = GetTextSubtitlesFilter(state, false, false);
+ mainFilters.Add(textSubtitlesFilter);
+ }
+ }
+
+ if (memoryOutput && isVaapiEncoder)
+ {
+ if (!hasGraphicalSubs)
+ {
+ mainFilters.Add("hwupload_vaapi");
+ }
+ }
+
+ /* Make sub and overlay filters for subtitle stream */
+ var subFilters = new List<string>();
+ var overlayFilters = new List<string>();
+ if (isVaInVaOut)
+ {
+ if (hasSubs)
+ {
+ if (hasGraphicalSubs)
+ {
+ // scale=s=1280x720,format=bgra,hwupload
+ var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ subFilters.Add(subSwScaleFilter);
+ subFilters.Add("format=bgra");
+ }
+ else if (hasTextSubs)
+ {
+ var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, hasAssSubs ? 10 : 5);
+ var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
+ subFilters.Add(alphaSrcFilter);
+ subFilters.Add("format=bgra");
+ subFilters.Add(subTextSubtitlesFilter);
+ }
+
+ subFilters.Add("hwupload=derive_device=vulkan:extra_hw_frames=16");
+
+ overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0");
+
+ // explicitly sync using libplacebo.
+ overlayFilters.Add("libplacebo=format=nv12:upscaler=none:downscaler=none");
+
+ // OUTPUT vaapi(nv12/bgra) surface(vram)
+ // reverse-mapping via vaapi-vulkan interop.
+ overlayFilters.Add("hwmap=derive_device=vaapi:reverse=1");
+ overlayFilters.Add("format=vaapi");
+ }
+ }
+ else if (memoryOutput)
+ {
+ if (hasGraphicalSubs)
+ {
+ var subSwScaleFilter = isSwDecoder
+ ? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
+ : GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ subFilters.Add(subSwScaleFilter);
+ overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
+
+ if (isVaapiEncoder)
+ {
+ overlayFilters.Add("hwupload_vaapi");
+ }
+ }
+ }
+
+ return (mainFilters, subFilters, overlayFilters);
+ }
+
public (List<string> MainFilters, List<string> SubFilters, List<string> OverlayFilters) GetVaapiLimitedVidFiltersPrefered(
EncodingJobInfo state,
EncodingOptions options,
diff --git a/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs
index a4869cb67..b1d319d21 100644
--- a/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs
+++ b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs
@@ -28,6 +28,11 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// The overlay_vaapi_framesync.
/// </summary>
- OverlayVaapiFrameSync = 4
+ OverlayVaapiFrameSync = 4,
+
+ /// <summary>
+ /// The overlay_vulkan_framesync.
+ /// </summary>
+ OverlayVulkanFrameSync = 5
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index 69d0bf45c..52c57b906 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -62,6 +62,12 @@ namespace MediaBrowser.Controller.MediaEncoding
bool IsVaapiDeviceInteli965 { get; }
/// <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 IsVaapiDeviceSupportVulkanFmtModifier { get; }
+
+ /// <summary>
/// Whether given encoder codec is supported.
/// </summary>
/// <param name="encoder">The encoder.</param>
diff --git a/MediaBrowser.Controller/Properties/AssemblyInfo.cs b/MediaBrowser.Controller/Properties/AssemblyInfo.cs
index 60e792309..534dec8d2 100644
--- a/MediaBrowser.Controller/Properties/AssemblyInfo.cs
+++ b/MediaBrowser.Controller/Properties/AssemblyInfo.cs
@@ -1,5 +1,6 @@
using System.Reflection;
using System.Resources;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
@@ -14,6 +15,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")]
+[assembly: InternalsVisibleTo("Jellyfin.Controller.Tests")]
+[assembly: InternalsVisibleTo("Jellyfin.Server.Implementations.Tests")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
diff --git a/MediaBrowser.Controller/Resolvers/ItemResolver.cs b/MediaBrowser.Controller/Resolvers/ItemResolver.cs
index 7fd54fcc6..e7bf013fa 100644
--- a/MediaBrowser.Controller/Resolvers/ItemResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/ItemResolver.cs
@@ -23,7 +23,7 @@ namespace MediaBrowser.Controller.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>`0.</returns>
- public virtual T Resolve(ItemResolveArgs args)
+ protected internal virtual T Resolve(ItemResolveArgs args)
{
return null;
}
@@ -42,7 +42,7 @@ namespace MediaBrowser.Controller.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>BaseItem.</returns>
- BaseItem IItemResolver.ResolvePath(ItemResolveArgs args)
+ public BaseItem ResolvePath(ItemResolveArgs args)
{
var item = Resolve(args);
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 9b4b1db94..8c8fc6b0f 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -102,7 +102,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
"tonemap_vaapi",
"procamp_vaapi",
"overlay_vaapi",
- "hwupload_vaapi"
+ "hwupload_vaapi",
+ // vulkan
+ "libplacebo",
+ "scale_vulkan",
+ "overlay_vulkan"
};
private static readonly IReadOnlyDictionary<int, string[]> _filterOptionsDict = new Dictionary<int, string[]>
@@ -111,7 +115,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
{ 1, new string[] { "tonemap_cuda", "GPU accelerated HDR to SDR tonemapping" } },
{ 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" } }
+ { 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" } }
};
// These are the library versions that corresponds to our minimum ffmpeg version 4.x according to the version table below
@@ -351,6 +356,39 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
+ public bool CheckVulkanDrmDeviceByExtensionName(string renderNodePath, string[] vulkanExtensions)
+ {
+ if (!OperatingSystem.IsLinux())
+ {
+ return false;
+ }
+
+ if (string.IsNullOrEmpty(renderNodePath))
+ {
+ return false;
+ }
+
+ try
+ {
+ var command = "-v verbose -hide_banner -init_hw_device drm=dr:" + renderNodePath + " -init_hw_device vulkan=vk@dr";
+ var output = GetProcessOutput(_encoderPath, command, true, null);
+ foreach (string ext in vulkanExtensions)
+ {
+ if (!output.Contains(ext, StringComparison.Ordinal))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error detecting the given drm render node path");
+ return false;
+ }
+ }
+
private IEnumerable<string> GetHwaccelTypes()
{
string? output = null;
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 757a01715..ec3412f90 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -72,6 +72,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
private bool _isVaapiDeviceAmd = false;
private bool _isVaapiDeviceInteliHD = false;
private bool _isVaapiDeviceInteli965 = false;
+ private bool _isVaapiDeviceSupportVulkanFmtModifier = false;
+
+ private static string[] _vulkanFmtModifierExts = {
+ "VK_KHR_sampler_ycbcr_conversion",
+ "VK_EXT_image_drm_format_modifier",
+ "VK_KHR_external_memory_fd",
+ "VK_EXT_external_memory_dma_buf",
+ "VK_KHR_external_semaphore_fd",
+ "VK_EXT_external_memory_host"
+ };
private Version _ffmpegVersion = null;
private string _ffmpegPath = string.Empty;
@@ -110,6 +120,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
public bool IsVaapiDeviceInteli965 => _isVaapiDeviceInteli965;
+ public bool IsVaapiDeviceSupportVulkanFmtModifier => _isVaapiDeviceSupportVulkanFmtModifier;
+
/// <summary>
/// Run at startup or if the user removes a Custom path from transcode page.
/// Sets global variables FFmpegPath.
@@ -169,6 +181,8 @@ 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);
+ _isVaapiDeviceSupportVulkanFmtModifier = validator.CheckVulkanDrmDeviceByExtensionName(options.VaapiDevice, _vulkanFmtModifierExts);
+
if (_isVaapiDeviceAmd)
{
_logger.LogInformation("VAAPI device {RenderNodePath} is AMD GPU", options.VaapiDevice);
@@ -181,6 +195,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
_logger.LogInformation("VAAPI device {RenderNodePath} is Intel GPU (i965)", options.VaapiDevice);
}
+
+ if (_isVaapiDeviceSupportVulkanFmtModifier)
+ {
+ _logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM format modifier", options.VaapiDevice);
+ }
}
}
diff --git a/MediaBrowser.Model/IO/IZipClient.cs b/MediaBrowser.Model/IO/IZipClient.cs
deleted file mode 100644
index 2448575d1..000000000
--- a/MediaBrowser.Model/IO/IZipClient.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-#pragma warning disable CS1591
-
-using System.IO;
-
-namespace MediaBrowser.Model.IO
-{
- /// <summary>
- /// Interface IZipClient.
- /// </summary>
- public interface IZipClient
- {
- void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles);
-
- void ExtractFirstFileFromGz(Stream source, string targetPath, string defaultFileName);
- }
-}
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 3a0e9a225..b00c036e5 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -17,6 +17,7 @@
<ItemGroup>
<PackageReference Include="LrcParser" Version="2022.529.1" />
+ <PackageReference Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs
index 9c27bd7d3..22229e377 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs
@@ -1,37 +1,52 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Model.Plugins;
+using MetaBrainz.MusicBrainz;
+
+namespace MediaBrowser.Providers.Plugins.MusicBrainz.Configuration;
-namespace MediaBrowser.Providers.Plugins.MusicBrainz
+/// <summary>
+/// MusicBrainz plugin configuration.
+/// </summary>
+public class PluginConfiguration : BasePluginConfiguration
{
- public class PluginConfiguration : BasePluginConfiguration
- {
- private string _server = Plugin.DefaultServer;
+ private const string DefaultServer = "musicbrainz.org";
- private long _rateLimit = Plugin.DefaultRateLimit;
+ private const double DefaultRateLimit = 1.0;
- public string Server
- {
- get => _server;
- set => _server = value.TrimEnd('/');
- }
+ private string _server = DefaultServer;
+
+ private double _rateLimit = DefaultRateLimit;
+
+ /// <summary>
+ /// Gets or sets the server url.
+ /// </summary>
+ public string Server
+ {
+ get => _server;
- public long RateLimit
+ set => _server = value.TrimEnd('/');
+ }
+
+ /// <summary>
+ /// Gets or sets the rate limit.
+ /// </summary>
+ public double RateLimit
+ {
+ get => _rateLimit;
+ set
{
- get => _rateLimit;
- set
+ if (value < DefaultRateLimit && _server == DefaultServer)
{
- if (value < Plugin.DefaultRateLimit && _server == Plugin.DefaultServer)
- {
- _rateLimit = Plugin.DefaultRateLimit;
- }
- else
- {
- _rateLimit = value;
- }
+ _rateLimit = DefaultRateLimit;
+ }
+ else
+ {
+ _rateLimit = value;
}
}
-
- public bool ReplaceArtistName { get; set; }
}
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to replace the artist name.
+ /// </summary>
+ public bool ReplaceArtistName { get; set; }
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs
index c54cdda3d..f7850781e 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs
@@ -1,28 +1,27 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Providers.Plugins.MusicBrainz;
-namespace MediaBrowser.Providers.Music
+namespace MediaBrowser.Providers.Plugins.MusicBrainz;
+
+/// <summary>
+/// MusicBrainz album artist external id.
+/// </summary>
+public class MusicBrainzAlbumArtistExternalId : IExternalId
{
- public class MusicBrainzAlbumArtistExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "MusicBrainz";
+ /// <inheritdoc />
+ public string ProviderName => "MusicBrainz";
- /// <inheritdoc />
- public string Key => MetadataProvider.MusicBrainzAlbumArtist.ToString();
+ /// <inheritdoc />
+ public string Key => MetadataProvider.MusicBrainzAlbumArtist.ToString();
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist;
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist;
- /// <inheritdoc />
- public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
+ /// <inheritdoc />
+ public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is Audio;
- }
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Audio;
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs
index 8f7fadd06..a9d4472e7 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs
@@ -1,28 +1,27 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Providers.Plugins.MusicBrainz;
-namespace MediaBrowser.Providers.Music
+namespace MediaBrowser.Providers.Plugins.MusicBrainz;
+
+/// <summary>
+/// MusicBrainz album external id.
+/// </summary>
+public class MusicBrainzAlbumExternalId : IExternalId
{
- public class MusicBrainzAlbumExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "MusicBrainz";
+ /// <inheritdoc />
+ public string ProviderName => "MusicBrainz";
- /// <inheritdoc />
- public string Key => MetadataProvider.MusicBrainzAlbum.ToString();
+ /// <inheritdoc />
+ public string Key => MetadataProvider.MusicBrainzAlbum.ToString();
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
- /// <inheritdoc />
- public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/release/{0}";
+ /// <inheritdoc />
+ public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/release/{0}";
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
- }
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
index 915fb97fd..4d9feca6d 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
@@ -1,805 +1,265 @@
-#nullable disable
-
-#pragma warning disable CS1591, SA1401
-
using System;
using System.Collections.Generic;
-using System.Diagnostics;
-using System.Globalization;
-using System.IO;
using System.Linq;
-using System.Net;
using System.Net.Http;
-using System.Text;
using System.Threading;
using System.Threading.Tasks;
-using System.Xml;
-using MediaBrowser.Common.Net;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Providers.Plugins.MusicBrainz;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Providers.Music
-{
- public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder, IDisposable
- {
- /// <summary>
- /// For each single MB lookup/search, this is the maximum number of
- /// attempts that shall be made whilst receiving a 503 Server
- /// Unavailable (indicating throttled) response.
- /// </summary>
- private const uint MusicBrainzQueryAttempts = 5u;
-
- /// <summary>
- /// The Jellyfin user-agent is unrestricted but source IP must not exceed
- /// one request per second, therefore we rate limit to avoid throttling.
- /// Be prudent, use a value slightly above the minimum required.
- /// https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting.
- /// </summary>
- private readonly long _musicBrainzQueryIntervalMs;
-
- private readonly IHttpClientFactory _httpClientFactory;
- private readonly ILogger<MusicBrainzAlbumProvider> _logger;
-
- private readonly string _musicBrainzBaseUrl;
-
- private SemaphoreSlim _apiRequestLock = new SemaphoreSlim(1, 1);
- private Stopwatch _stopWatchMusicBrainz = new Stopwatch();
-
- public MusicBrainzAlbumProvider(
- IHttpClientFactory httpClientFactory,
- ILogger<MusicBrainzAlbumProvider> logger)
- {
- _httpClientFactory = httpClientFactory;
- _logger = logger;
-
- _musicBrainzBaseUrl = Plugin.Instance.Configuration.Server;
- _musicBrainzQueryIntervalMs = Plugin.Instance.Configuration.RateLimit;
-
- // Use a stopwatch to ensure we don't exceed the MusicBrainz rate limit
- _stopWatchMusicBrainz.Start();
-
- Current = this;
- }
-
- internal static MusicBrainzAlbumProvider Current { get; private set; }
-
- /// <inheritdoc />
- public string Name => "MusicBrainz";
+using MediaBrowser.Providers.Music;
+using MetaBrainz.MusicBrainz;
+using MetaBrainz.MusicBrainz.Interfaces.Entities;
+using MetaBrainz.MusicBrainz.Interfaces.Searches;
- /// <inheritdoc />
- public int Order => 0;
+namespace MediaBrowser.Providers.Plugins.MusicBrainz;
- /// <inheritdoc />
- public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancellationToken)
- {
- var releaseId = searchInfo.GetReleaseId();
- var releaseGroupId = searchInfo.GetReleaseGroupId();
-
- string url;
-
- if (!string.IsNullOrEmpty(releaseId))
- {
- url = "/ws/2/release/?query=reid:" + releaseId.ToString(CultureInfo.InvariantCulture);
- }
- else if (!string.IsNullOrEmpty(releaseGroupId))
- {
- url = "/ws/2/release?release-group=" + releaseGroupId.ToString(CultureInfo.InvariantCulture);
- }
- else
- {
- var artistMusicBrainzId = searchInfo.GetMusicBrainzArtistId();
-
- if (!string.IsNullOrWhiteSpace(artistMusicBrainzId))
- {
- url = string.Format(
- CultureInfo.InvariantCulture,
- "/ws/2/release/?query=\"{0}\" AND arid:{1}",
- WebUtility.UrlEncode(searchInfo.Name),
- artistMusicBrainzId);
- }
- else
- {
- // I'm sure there is a better way but for now it resolves search for 12" Mixes
- var queryName = searchInfo.Name.Replace("\"", string.Empty, StringComparison.Ordinal);
-
- url = string.Format(
- CultureInfo.InvariantCulture,
- "/ws/2/release/?query=\"{0}\" AND artist:\"{1}\"",
- WebUtility.UrlEncode(queryName),
- WebUtility.UrlEncode(searchInfo.GetAlbumArtist()));
- }
- }
-
- if (!string.IsNullOrWhiteSpace(url))
- {
- using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- return GetResultsFromResponse(stream);
- }
-
- return Enumerable.Empty<RemoteSearchResult>();
- }
+/// <summary>
+/// Music album metadata provider for MusicBrainz.
+/// </summary>
+public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder, IDisposable
+{
+ private readonly Query _musicBrainzQuery;
- private IEnumerable<RemoteSearchResult> GetResultsFromResponse(Stream stream)
- {
- using var oReader = new StreamReader(stream, Encoding.UTF8);
- var settings = new XmlReaderSettings()
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MusicBrainzAlbumProvider"/> class.
+ /// </summary>
+ public MusicBrainzAlbumProvider()
+ {
+ MusicBrainz.Plugin.Instance!.ConfigurationChanged += (_, _) =>
{
- ValidationType = ValidationType.None,
- CheckCharacters = false,
- IgnoreProcessingInstructions = true,
- IgnoreComments = true
+ Query.DefaultServer = MusicBrainz.Plugin.Instance.Configuration.Server;
+ Query.DelayBetweenRequests = MusicBrainz.Plugin.Instance.Configuration.RateLimit;
};
- using var reader = XmlReader.Create(oReader, settings);
- var results = ReleaseResult.Parse(reader);
-
- return results.Select(i =>
- {
- var result = new RemoteSearchResult
- {
- Name = i.Title,
- ProductionYear = i.Year
- };
+ _musicBrainzQuery = new Query();
+ }
- if (i.Artists.Count > 0)
- {
- result.AlbumArtist = new RemoteSearchResult
- {
- SearchProviderName = Name,
- Name = i.Artists[0].Item1
- };
+ /// <inheritdoc />
+ public string Name => "MusicBrainz";
- result.AlbumArtist.SetProviderId(MetadataProvider.MusicBrainzArtist, i.Artists[0].Item2);
- }
+ /// <inheritdoc />
+ public int Order => 0;
- if (!string.IsNullOrWhiteSpace(i.ReleaseId))
- {
- result.SetProviderId(MetadataProvider.MusicBrainzAlbum, i.ReleaseId);
- }
-
- if (!string.IsNullOrWhiteSpace(i.ReleaseGroupId))
- {
- result.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, i.ReleaseGroupId);
- }
+ /// <inheritdoc />
+ public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancellationToken)
+ {
+ var releaseId = searchInfo.GetReleaseId();
+ var releaseGroupId = searchInfo.GetReleaseGroupId();
- return result;
- });
+ if (!string.IsNullOrEmpty(releaseId))
+ {
+ var releaseResult = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.ReleaseGroups, cancellationToken).ConfigureAwait(false);
+ return GetReleaseResult(releaseResult).SingleItemAsEnumerable();
}
- /// <inheritdoc />
- public async Task<MetadataResult<MusicAlbum>> GetMetadata(AlbumInfo info, CancellationToken cancellationToken)
+ if (!string.IsNullOrEmpty(releaseGroupId))
{
- var releaseId = info.GetReleaseId();
- var releaseGroupId = info.GetReleaseGroupId();
-
- var result = new MetadataResult<MusicAlbum>
- {
- Item = new MusicAlbum()
- };
-
- // If we have a release group Id but not a release Id...
- if (string.IsNullOrWhiteSpace(releaseId) && !string.IsNullOrWhiteSpace(releaseGroupId))
- {
- releaseId = await GetReleaseIdFromReleaseGroupId(releaseGroupId, cancellationToken).ConfigureAwait(false);
- result.HasMetadata = true;
- }
-
- if (string.IsNullOrWhiteSpace(releaseId))
- {
- var artistMusicBrainzId = info.GetMusicBrainzArtistId();
-
- var releaseResult = await GetReleaseResult(artistMusicBrainzId, info.GetAlbumArtist(), info.Name, cancellationToken).ConfigureAwait(false);
-
- if (releaseResult != null)
- {
- if (!string.IsNullOrWhiteSpace(releaseResult.ReleaseId))
- {
- releaseId = releaseResult.ReleaseId;
- result.HasMetadata = true;
- }
-
- if (!string.IsNullOrWhiteSpace(releaseResult.ReleaseGroupId))
- {
- releaseGroupId = releaseResult.ReleaseGroupId;
- result.HasMetadata = true;
- }
-
- result.Item.ProductionYear = releaseResult.Year;
- result.Item.Overview = releaseResult.Overview;
- }
- }
+ var releaseGroupResult = await _musicBrainzQuery.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.None, null, cancellationToken).ConfigureAwait(false);
+ return GetReleaseGroupResult(releaseGroupResult.Releases);
+ }
- // If we have a release Id but not a release group Id...
- if (!string.IsNullOrWhiteSpace(releaseId) && string.IsNullOrWhiteSpace(releaseGroupId))
- {
- releaseGroupId = await GetReleaseGroupFromReleaseId(releaseId, cancellationToken).ConfigureAwait(false);
- result.HasMetadata = true;
- }
+ var artistMusicBrainzId = searchInfo.GetMusicBrainzArtistId();
- if (!string.IsNullOrWhiteSpace(releaseId) || !string.IsNullOrWhiteSpace(releaseGroupId))
- {
- result.HasMetadata = true;
- }
+ if (!string.IsNullOrWhiteSpace(artistMusicBrainzId))
+ {
+ var releaseSearchResults = await _musicBrainzQuery.FindReleasesAsync($"\"{searchInfo.Name}\" AND arid:{artistMusicBrainzId}", null, null, false, cancellationToken)
+ .ConfigureAwait(false);
- if (result.HasMetadata)
+ if (releaseSearchResults.Results.Count > 0)
{
- if (!string.IsNullOrEmpty(releaseId))
- {
- result.Item.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseId);
- }
-
- if (!string.IsNullOrEmpty(releaseGroupId))
- {
- result.Item.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, releaseGroupId);
- }
+ return GetReleaseSearchResult(releaseSearchResults.Results);
}
-
- return result;
}
-
- private Task<ReleaseResult> GetReleaseResult(string artistMusicBrainId, string artistName, string albumName, CancellationToken cancellationToken)
+ else
{
- if (!string.IsNullOrEmpty(artistMusicBrainId))
- {
- return GetReleaseResult(albumName, artistMusicBrainId, cancellationToken);
- }
+ // I'm sure there is a better way but for now it resolves search for 12" Mixes
+ var queryName = searchInfo.Name.Replace("\"", string.Empty, StringComparison.Ordinal);
- if (string.IsNullOrWhiteSpace(artistName))
+ var releaseSearchResults = await _musicBrainzQuery.FindReleasesAsync($"\"{queryName}\" AND artist:\"{searchInfo.GetAlbumArtist()}\"c", null, null, false, cancellationToken)
+ .ConfigureAwait(false);
+
+ if (releaseSearchResults.Results.Count > 0)
{
- return Task.FromResult(new ReleaseResult());
+ return GetReleaseSearchResult(releaseSearchResults.Results);
}
-
- return GetReleaseResultByArtistName(albumName, artistName, cancellationToken);
}
- private async Task<ReleaseResult> GetReleaseResult(string albumName, string artistId, CancellationToken cancellationToken)
- {
- var url = string.Format(
- CultureInfo.InvariantCulture,
- "/ws/2/release/?query=\"{0}\" AND arid:{1}",
- WebUtility.UrlEncode(albumName),
- artistId);
-
- using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- using var oReader = new StreamReader(stream, Encoding.UTF8);
- var settings = new XmlReaderSettings
- {
- ValidationType = ValidationType.None,
- CheckCharacters = false,
- IgnoreProcessingInstructions = true,
- IgnoreComments = true
- };
+ return Enumerable.Empty<RemoteSearchResult>();
+ }
- using var reader = XmlReader.Create(oReader, settings);
- return ReleaseResult.Parse(reader).FirstOrDefault();
+ private IEnumerable<RemoteSearchResult> GetReleaseSearchResult(IEnumerable<ISearchResult<IRelease>>? releaseSearchResults)
+ {
+ if (releaseSearchResults is null)
+ {
+ yield break;
}
- private async Task<ReleaseResult> GetReleaseResultByArtistName(string albumName, string artistName, CancellationToken cancellationToken)
+ foreach (var result in releaseSearchResults)
{
- var url = string.Format(
- CultureInfo.InvariantCulture,
- "/ws/2/release/?query=\"{0}\" AND artist:\"{1}\"",
- WebUtility.UrlEncode(albumName),
- WebUtility.UrlEncode(artistName));
-
- using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- using var oReader = new StreamReader(stream, Encoding.UTF8);
- var settings = new XmlReaderSettings()
- {
- ValidationType = ValidationType.None,
- CheckCharacters = false,
- IgnoreProcessingInstructions = true,
- IgnoreComments = true
- };
-
- using var reader = XmlReader.Create(oReader, settings);
- return ReleaseResult.Parse(reader).FirstOrDefault();
+ yield return GetReleaseResult(result.Item);
}
+ }
- private static (string Name, string ArtistId) ParseArtistCredit(XmlReader reader)
+ private IEnumerable<RemoteSearchResult> GetReleaseGroupResult(IEnumerable<IRelease>? releaseSearchResults)
+ {
+ if (releaseSearchResults is null)
{
- reader.MoveToContent();
- reader.Read();
-
- // http://stackoverflow.com/questions/2299632/why-does-xmlreader-skip-every-other-element-if-there-is-no-whitespace-separator
-
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "name-credit":
- {
- if (reader.IsEmptyElement)
- {
- reader.Read();
- break;
- }
-
- using var subReader = reader.ReadSubtree();
- return ParseArtistNameCredit(subReader);
- }
-
- default:
- {
- reader.Skip();
- break;
- }
- }
- }
- else
- {
- reader.Read();
- }
- }
-
- return default;
+ yield break;
}
- private static (string Name, string ArtistId) ParseArtistNameCredit(XmlReader reader)
+ foreach (var result in releaseSearchResults)
{
- reader.MoveToContent();
- reader.Read();
-
- // http://stackoverflow.com/questions/2299632/why-does-xmlreader-skip-every-other-element-if-there-is-no-whitespace-separator
-
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "artist":
- {
- if (reader.IsEmptyElement)
- {
- reader.Read();
- break;
- }
-
- var id = reader.GetAttribute("id");
- using var subReader = reader.ReadSubtree();
- return ParseArtistArtistCredit(subReader, id);
- }
-
- default:
- {
- reader.Skip();
- break;
- }
- }
- }
- else
- {
- reader.Read();
- }
- }
-
- return (null, null);
+ yield return GetReleaseResult(result);
}
+ }
- private static (string Name, string ArtistId) ParseArtistArtistCredit(XmlReader reader, string artistId)
+ private RemoteSearchResult GetReleaseResult(IRelease releaseSearchResult)
+ {
+ var searchResult = new RemoteSearchResult
{
- reader.MoveToContent();
- reader.Read();
-
- string name = null;
+ Name = releaseSearchResult.Title,
+ ProductionYear = releaseSearchResult.Date?.Year,
+ PremiereDate = releaseSearchResult.Date?.NearestDate
+ };
- // http://stackoverflow.com/questions/2299632/why-does-xmlreader-skip-every-other-element-if-there-is-no-whitespace-separator
+ if (releaseSearchResult.ArtistCredit?.Count > 0)
+ {
+ searchResult.AlbumArtist = new RemoteSearchResult
+ {
+ SearchProviderName = Name,
+ Name = releaseSearchResult.ArtistCredit[0].Name
+ };
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ if (releaseSearchResult.ArtistCredit[0].Artist?.Id is not null)
{
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "name":
- {
- name = reader.ReadElementContentAsString();
- break;
- }
-
- default:
- {
- reader.Skip();
- break;
- }
- }
- }
- else
- {
- reader.Read();
- }
+ searchResult.AlbumArtist.SetProviderId(MetadataProvider.MusicBrainzArtist, releaseSearchResult.ArtistCredit[0].Artist!.Id.ToString());
}
-
- return (name, artistId);
}
- private async Task<string> GetReleaseIdFromReleaseGroupId(string releaseGroupId, CancellationToken cancellationToken)
- {
- var url = "/ws/2/release?release-group=" + releaseGroupId.ToString(CultureInfo.InvariantCulture);
-
- using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- using var oReader = new StreamReader(stream, Encoding.UTF8);
- var settings = new XmlReaderSettings
- {
- ValidationType = ValidationType.None,
- CheckCharacters = false,
- IgnoreProcessingInstructions = true,
- IgnoreComments = true
- };
-
- using var reader = XmlReader.Create(oReader, settings);
- var result = ReleaseResult.Parse(reader).FirstOrDefault();
+ searchResult.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseSearchResult.Id.ToString());
- return result?.ReleaseId;
+ if (releaseSearchResult.ReleaseGroup?.Id is not null)
+ {
+ searchResult.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, releaseSearchResult.ReleaseGroup.Id.ToString());
}
- /// <summary>
- /// Gets the release group id internal.
- /// </summary>
- /// <param name="releaseEntryId">The release entry id.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task{System.String}.</returns>
- private async Task<string> GetReleaseGroupFromReleaseId(string releaseEntryId, CancellationToken cancellationToken)
- {
- var url = "/ws/2/release-group/?query=reid:" + releaseEntryId.ToString(CultureInfo.InvariantCulture);
+ return searchResult;
+ }
- using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- using var oReader = new StreamReader(stream, Encoding.UTF8);
- var settings = new XmlReaderSettings
- {
- ValidationType = ValidationType.None,
- CheckCharacters = false,
- IgnoreProcessingInstructions = true,
- IgnoreComments = true,
- Async = true
- };
+ /// <inheritdoc />
+ public async Task<MetadataResult<MusicAlbum>> GetMetadata(AlbumInfo info, CancellationToken cancellationToken)
+ {
+ // TODO: This sets essentially nothing. As-is, it's mostly useless. Make it actually pull metadata and use it.
+ var releaseId = info.GetReleaseId();
+ var releaseGroupId = info.GetReleaseGroupId();
- using var reader = XmlReader.Create(oReader, settings);
- await reader.MoveToContentAsync().ConfigureAwait(false);
- await reader.ReadAsync().ConfigureAwait(false);
+ var result = new MetadataResult<MusicAlbum>
+ {
+ Item = new MusicAlbum()
+ };
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ // If there is a release group, but no release ID, try to match the release
+ if (string.IsNullOrWhiteSpace(releaseId) && !string.IsNullOrWhiteSpace(releaseGroupId))
+ {
+ // TODO: Actually try to match the release. Simply taking the first result is stupid.
+ var releaseGroup = await _musicBrainzQuery.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.None, null, cancellationToken).ConfigureAwait(false);
+ var release = releaseGroup.Releases?.Count > 0 ? releaseGroup.Releases[0] : null;
+ if (release != null)
{
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "release-group-list":
- {
- if (reader.IsEmptyElement)
- {
- await reader.ReadAsync().ConfigureAwait(false);
- continue;
- }
-
- using var subReader = reader.ReadSubtree();
- return GetFirstReleaseGroupId(subReader);
- }
-
- default:
- {
- await reader.SkipAsync().ConfigureAwait(false);
- break;
- }
- }
- }
- else
- {
- await reader.ReadAsync().ConfigureAwait(false);
- }
+ releaseId = release.Id.ToString();
+ result.HasMetadata = true;
}
-
- return null;
}
- private string GetFirstReleaseGroupId(XmlReader reader)
+ // If there is no release ID, lookup a release with the info we have
+ if (string.IsNullOrWhiteSpace(releaseId))
{
- reader.MoveToContent();
- reader.Read();
+ var artistMusicBrainzId = info.GetMusicBrainzArtistId();
+ IRelease? releaseResult = null;
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ if (!string.IsNullOrEmpty(artistMusicBrainzId))
{
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "release-group":
- {
- return reader.GetAttribute("id");
- }
-
- default:
- {
- reader.Skip();
- break;
- }
- }
- }
- else
- {
- reader.Read();
- }
+ var releaseSearchResults = await _musicBrainzQuery.FindReleasesAsync($"\"{info.Name}\" AND arid:{artistMusicBrainzId}", null, null, false, cancellationToken)
+ .ConfigureAwait(false);
+ releaseResult = releaseSearchResults.Results.Count > 0 ? releaseSearchResults.Results[0].Item : null;
}
-
- return null;
- }
-
- /// <summary>
- /// Makes request to MusicBrainz server and awaits a response.
- /// A 503 Service Unavailable response indicates throttling to maintain a rate limit.
- /// A number of retries shall be made in order to try and satisfy the request before
- /// giving up and returning null.
- /// </summary>
- /// <param name="url">Address of MusicBrainz server.</param>
- /// <param name="cancellationToken">CancellationToken to use for method.</param>
- /// <returns>Returns response from MusicBrainz service.</returns>
- internal async Task<HttpResponseMessage> GetMusicBrainzResponse(string url, CancellationToken cancellationToken)
- {
- await _apiRequestLock.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- try
+ else if (!string.IsNullOrEmpty(info.GetAlbumArtist()))
{
- HttpResponseMessage response;
- var attempts = 0u;
- var requestUrl = _musicBrainzBaseUrl.TrimEnd('/') + url;
+ var releaseSearchResults = await _musicBrainzQuery.FindReleasesAsync($"\"{info.Name}\" AND artist:{info.GetAlbumArtist()}", null, null, false, cancellationToken)
+ .ConfigureAwait(false);
+ releaseResult = releaseSearchResults.Results.Count > 0 ? releaseSearchResults.Results[0].Item : null;
+ }
- do
- {
- attempts++;
-
- if (_stopWatchMusicBrainz.ElapsedMilliseconds < _musicBrainzQueryIntervalMs)
- {
- // MusicBrainz is extremely adamant about limiting to one request per second.
- var delayMs = _musicBrainzQueryIntervalMs - _stopWatchMusicBrainz.ElapsedMilliseconds;
- await Task.Delay((int)delayMs, cancellationToken).ConfigureAwait(false);
- }
-
- // Write time since last request to debug log as evidence we're meeting rate limit
- // requirement, before resetting stopwatch back to zero.
- _logger.LogDebug("GetMusicBrainzResponse: Time since previous request: {0} ms", _stopWatchMusicBrainz.ElapsedMilliseconds);
- _stopWatchMusicBrainz.Restart();
-
- using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
- response = await _httpClientFactory
- .CreateClient(NamedClient.MusicBrainz)
- .SendAsync(request, cancellationToken)
- .ConfigureAwait(false);
-
- // We retry a finite number of times, and only whilst MB is indicating 503 (throttling).
- }
- while (attempts < MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable);
+ if (releaseResult != null)
+ {
+ releaseId = releaseResult.Id.ToString();
- // Log error if unable to query MB database due to throttling.
- if (attempts == MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable)
+ if (releaseResult.ReleaseGroup?.Id is not null)
{
- _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, requestUrl);
+ releaseGroupId = releaseResult.ReleaseGroup.Id.ToString();
}
- return response;
- }
- finally
- {
- _apiRequestLock.Release();
+ result.HasMetadata = true;
+ result.Item.ProductionYear = releaseResult.Date?.Year;
+ result.Item.Overview = releaseResult.Annotation;
}
}
- /// <inheritdoc />
- public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
+ // If we have a release ID but not a release group ID, lookup the release group
+ if (!string.IsNullOrWhiteSpace(releaseId) && string.IsNullOrWhiteSpace(releaseGroupId))
{
- throw new NotImplementedException();
+ var release = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.Releases, cancellationToken).ConfigureAwait(false);
+ releaseGroupId = release.ReleaseGroup?.Id.ToString();
+ result.HasMetadata = true;
}
- protected virtual void Dispose(bool disposing)
+ // If we have a release ID and a release group ID
+ if (!string.IsNullOrWhiteSpace(releaseId) || !string.IsNullOrWhiteSpace(releaseGroupId))
{
- if (disposing)
- {
- _apiRequestLock?.Dispose();
- }
+ result.HasMetadata = true;
}
- /// <inheritdoc />
- public void Dispose()
+ if (result.HasMetadata)
{
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- private class ReleaseResult
- {
- public string ReleaseId;
- public string ReleaseGroupId;
- public string Title;
- public string Overview;
- public int? Year;
-
- public List<(string, string)> Artists = new();
-
- public static IEnumerable<ReleaseResult> Parse(XmlReader reader)
+ if (!string.IsNullOrEmpty(releaseId))
{
- reader.MoveToContent();
- reader.Read();
-
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "release-list":
- {
- if (reader.IsEmptyElement)
- {
- reader.Read();
- continue;
- }
-
- using var subReader = reader.ReadSubtree();
- return ParseReleaseList(subReader).ToList();
- }
-
- default:
- {
- reader.Skip();
- break;
- }
- }
- }
- else
- {
- reader.Read();
- }
- }
-
- return Enumerable.Empty<ReleaseResult>();
+ result.Item.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseId);
}
- private static IEnumerable<ReleaseResult> ParseReleaseList(XmlReader reader)
+ if (!string.IsNullOrEmpty(releaseGroupId))
{
- reader.MoveToContent();
- reader.Read();
-
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "release":
- {
- if (reader.IsEmptyElement)
- {
- reader.Read();
- continue;
- }
-
- var releaseId = reader.GetAttribute("id");
-
- using var subReader = reader.ReadSubtree();
- var release = ParseRelease(subReader, releaseId);
- if (release != null)
- {
- yield return release;
- }
-
- break;
- }
-
- default:
- {
- reader.Skip();
- break;
- }
- }
- }
- else
- {
- reader.Read();
- }
- }
+ result.Item.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, releaseGroupId);
}
+ }
- private static ReleaseResult ParseRelease(XmlReader reader, string releaseId)
- {
- var result = new ReleaseResult
- {
- ReleaseId = releaseId
- };
-
- reader.MoveToContent();
- reader.Read();
+ return result;
+ }
- // http://stackoverflow.com/questions/2299632/why-does-xmlreader-skip-every-other-element-if-there-is-no-whitespace-separator
+ /// <inheritdoc />
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "title":
- {
- result.Title = reader.ReadElementContentAsString();
- break;
- }
-
- case "date":
- {
- var val = reader.ReadElementContentAsString();
- if (DateTime.TryParse(val, out var date))
- {
- result.Year = date.Year;
- }
-
- break;
- }
-
- case "annotation":
- {
- result.Overview = reader.ReadElementContentAsString();
- break;
- }
-
- case "release-group":
- {
- result.ReleaseGroupId = reader.GetAttribute("id");
- reader.Skip();
- break;
- }
-
- case "artist-credit":
- {
- if (reader.IsEmptyElement)
- {
- reader.Read();
- break;
- }
-
- using var subReader = reader.ReadSubtree();
- var artist = ParseArtistCredit(subReader);
-
- if (!string.IsNullOrEmpty(artist.Name))
- {
- result.Artists.Add(artist);
- }
-
- break;
- }
-
- default:
- {
- reader.Skip();
- break;
- }
- }
- }
- else
- {
- reader.Read();
- }
- }
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
- return result;
- }
+ /// <summary>
+ /// Dispose all resources.
+ /// </summary>
+ /// <param name="disposing">Whether to dispose.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _musicBrainzQuery.Dispose();
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs
index 941ffea72..b89e67270 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs
@@ -1,28 +1,27 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Providers.Plugins.MusicBrainz;
-namespace MediaBrowser.Providers.Music
+namespace MediaBrowser.Providers.Plugins.MusicBrainz;
+
+/// <summary>
+/// MusicBrainz artist external id.
+/// </summary>
+public class MusicBrainzArtistExternalId : IExternalId
{
- public class MusicBrainzArtistExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "MusicBrainz";
+ /// <inheritdoc />
+ public string ProviderName => "MusicBrainz";
- /// <inheritdoc />
- public string Key => MetadataProvider.MusicBrainzArtist.ToString();
+ /// <inheritdoc />
+ public string Key => MetadataProvider.MusicBrainzArtist.ToString();
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
- /// <inheritdoc />
- public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
+ /// <inheritdoc />
+ public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is MusicArtist;
- }
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is MusicArtist;
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs
index 906a42f36..2cc3a13be 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs
@@ -1,15 +1,7 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
using System.Linq;
-using System.Net;
using System.Net.Http;
-using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
@@ -18,257 +10,152 @@ using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Providers.Plugins.MusicBrainz;
-
-namespace MediaBrowser.Providers.Music
-{
- public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>
- {
- public string Name => "MusicBrainz";
+using MediaBrowser.Providers.Music;
+using MetaBrainz.MusicBrainz;
+using MetaBrainz.MusicBrainz.Interfaces.Entities;
+using MetaBrainz.MusicBrainz.Interfaces.Searches;
- /// <inheritdoc />
- public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken)
- {
- var musicBrainzId = searchInfo.GetMusicBrainzArtistId();
+namespace MediaBrowser.Providers.Plugins.MusicBrainz;
- if (!string.IsNullOrWhiteSpace(musicBrainzId))
- {
- var url = "/ws/2/artist/?query=arid:{0}" + musicBrainzId.ToString(CultureInfo.InvariantCulture);
+/// <summary>
+/// MusicBrainz artist provider.
+/// </summary>
+public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>, IDisposable
+{
+ private readonly Query _musicBrainzQuery;
- using var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- return GetResultsFromResponse(stream);
- }
- else
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MusicBrainzArtistProvider"/> class.
+ /// </summary>
+ public MusicBrainzArtistProvider()
+ {
+ MusicBrainz.Plugin.Instance!.ConfigurationChanged += (_, _) =>
{
- // They seem to throw bad request failures on any term with a slash
- var nameToSearch = searchInfo.Name.Replace('/', ' ');
-
- var url = string.Format(CultureInfo.InvariantCulture, "/ws/2/artist/?query=\"{0}\"&dismax=true", UrlEncode(nameToSearch));
-
- using (var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false))
- await using (var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false))
- {
- var results = GetResultsFromResponse(stream).ToList();
+ Query.DefaultServer = MusicBrainz.Plugin.Instance.Configuration.Server;
+ Query.DelayBetweenRequests = MusicBrainz.Plugin.Instance.Configuration.RateLimit;
+ };
- if (results.Count > 0)
- {
- return results;
- }
- }
+ _musicBrainzQuery = new Query();
+ }
- if (searchInfo.Name.HasDiacritics())
- {
- // Try again using the search with accent characters url
- url = string.Format(CultureInfo.InvariantCulture, "/ws/2/artist/?query=artistaccent:\"{0}\"", UrlEncode(nameToSearch));
+ /// <inheritdoc />
+ public string Name => "MusicBrainz";
- using var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- return GetResultsFromResponse(stream);
- }
- }
+ /// <inheritdoc />
+ public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken)
+ {
+ var artistId = searchInfo.GetMusicBrainzArtistId();
- return Enumerable.Empty<RemoteSearchResult>();
+ if (!string.IsNullOrWhiteSpace(artistId))
+ {
+ var artistResult = await _musicBrainzQuery.LookupArtistAsync(new Guid(artistId), Include.Aliases, null, null, cancellationToken).ConfigureAwait(false);
+ return GetResultFromResponse(artistResult).SingleItemAsEnumerable();
}
- private IEnumerable<RemoteSearchResult> GetResultsFromResponse(Stream stream)
+ var artistSearchResults = await _musicBrainzQuery.FindArtistsAsync($"\"{searchInfo.Name}\"", null, null, false, cancellationToken)
+ .ConfigureAwait(false);
+ if (artistSearchResults.Results.Count > 0)
{
- using var oReader = new StreamReader(stream, Encoding.UTF8);
- var settings = new XmlReaderSettings()
- {
- ValidationType = ValidationType.None,
- CheckCharacters = false,
- IgnoreProcessingInstructions = true,
- IgnoreComments = true
- };
-
- using var reader = XmlReader.Create(oReader, settings);
- reader.MoveToContent();
- reader.Read();
-
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "artist-list":
- {
- if (reader.IsEmptyElement)
- {
- reader.Read();
- continue;
- }
-
- using var subReader = reader.ReadSubtree();
- return ParseArtistList(subReader).ToList();
- }
-
- default:
- {
- reader.Skip();
- break;
- }
- }
- }
- else
- {
- reader.Read();
- }
- }
-
- return Enumerable.Empty<RemoteSearchResult>();
+ return GetResultsFromResponse(artistSearchResults.Results);
}
- private IEnumerable<RemoteSearchResult> ParseArtistList(XmlReader reader)
+ if (searchInfo.Name.HasDiacritics())
{
- reader.MoveToContent();
- reader.Read();
-
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ // Try again using the search with an accented characters query
+ var artistAccentsSearchResults = await _musicBrainzQuery.FindArtistsAsync($"artistaccent:\"{searchInfo.Name}\"", null, null, false, cancellationToken)
+ .ConfigureAwait(false);
+ if (artistAccentsSearchResults.Results.Count > 0)
{
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "artist":
- {
- if (reader.IsEmptyElement)
- {
- reader.Read();
- continue;
- }
-
- var mbzId = reader.GetAttribute("id");
-
- using var subReader = reader.ReadSubtree();
- var artist = ParseArtist(subReader, mbzId);
- if (artist != null)
- {
- yield return artist;
- }
-
- break;
- }
-
- default:
- {
- reader.Skip();
- break;
- }
- }
- }
- else
- {
- reader.Read();
- }
+ return GetResultsFromResponse(artistAccentsSearchResults.Results);
}
}
- private RemoteSearchResult ParseArtist(XmlReader reader, string artistId)
- {
- var result = new RemoteSearchResult();
-
- reader.MoveToContent();
- reader.Read();
+ return Enumerable.Empty<RemoteSearchResult>();
+ }
- // http://stackoverflow.com/questions/2299632/why-does-xmlreader-skip-every-other-element-if-there-is-no-whitespace-separator
+ private IEnumerable<RemoteSearchResult> GetResultsFromResponse(IEnumerable<ISearchResult<IArtist>>? releaseSearchResults)
+ {
+ if (releaseSearchResults is null)
+ {
+ yield break;
+ }
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "name":
- {
- result.Name = reader.ReadElementContentAsString();
- break;
- }
+ foreach (var result in releaseSearchResults)
+ {
+ yield return GetResultFromResponse(result.Item);
+ }
+ }
- case "annotation":
- {
- result.Overview = reader.ReadElementContentAsString();
- break;
- }
+ private RemoteSearchResult GetResultFromResponse(IArtist artist)
+ {
+ var searchResult = new RemoteSearchResult
+ {
+ Name = artist.Name,
+ ProductionYear = artist.LifeSpan?.Begin?.Year,
+ PremiereDate = artist.LifeSpan?.Begin?.NearestDate
+ };
- default:
- {
- // there is sort-name if ever needed
- reader.Skip();
- break;
- }
- }
- }
- else
- {
- reader.Read();
- }
- }
+ searchResult.SetProviderId(MetadataProvider.MusicBrainzArtist, artist.Id.ToString());
- result.SetProviderId(MetadataProvider.MusicBrainzArtist, artistId);
+ return searchResult;
+ }
- if (string.IsNullOrWhiteSpace(artistId) || string.IsNullOrWhiteSpace(result.Name))
- {
- return null;
- }
+ /// <inheritdoc />
+ public async Task<MetadataResult<MusicArtist>> GetMetadata(ArtistInfo info, CancellationToken cancellationToken)
+ {
+ var result = new MetadataResult<MusicArtist> { Item = new MusicArtist() };
- return result;
- }
+ var musicBrainzId = info.GetMusicBrainzArtistId();
- /// <inheritdoc />
- public async Task<MetadataResult<MusicArtist>> GetMetadata(ArtistInfo info, CancellationToken cancellationToken)
+ if (string.IsNullOrWhiteSpace(musicBrainzId))
{
- var result = new MetadataResult<MusicArtist>
- {
- Item = new MusicArtist()
- };
+ var searchResults = await GetSearchResults(info, cancellationToken).ConfigureAwait(false);
- var musicBrainzId = info.GetMusicBrainzArtistId();
+ var singleResult = searchResults.FirstOrDefault();
- if (string.IsNullOrWhiteSpace(musicBrainzId))
+ if (singleResult != null)
{
- var searchResults = await GetSearchResults(info, cancellationToken).ConfigureAwait(false);
+ musicBrainzId = singleResult.GetProviderId(MetadataProvider.MusicBrainzArtist);
+ result.Item.Overview = singleResult.Overview;
- var singleResult = searchResults.FirstOrDefault();
-
- if (singleResult != null)
+ if (Plugin.Instance!.Configuration.ReplaceArtistName)
{
- musicBrainzId = singleResult.GetProviderId(MetadataProvider.MusicBrainzArtist);
- result.Item.Overview = singleResult.Overview;
-
- if (Plugin.Instance.Configuration.ReplaceArtistName)
- {
- result.Item.Name = singleResult.Name;
- }
+ result.Item.Name = singleResult.Name;
}
}
-
- if (!string.IsNullOrWhiteSpace(musicBrainzId))
- {
- result.HasMetadata = true;
- result.Item.SetProviderId(MetadataProvider.MusicBrainzArtist, musicBrainzId);
- }
-
- return result;
}
- /// <summary>
- /// Encodes an URL.
- /// </summary>
- /// <param name="name">The name.</param>
- /// <returns>System.String.</returns>
- private static string UrlEncode(string name)
+ if (!string.IsNullOrWhiteSpace(musicBrainzId))
{
- return WebUtility.UrlEncode(name);
+ result.HasMetadata = true;
+ result.Item.SetProviderId(MetadataProvider.MusicBrainzArtist, musicBrainzId);
}
- public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
+ return result;
+ }
+
+ /// <inheritdoc />
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Dispose all resources.
+ /// </summary>
+ /// <param name="disposing">Whether to dispose.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
{
- throw new NotImplementedException();
+ _musicBrainzQuery.Dispose();
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs
index 05db2d98f..fdaa5574f 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs
@@ -1,28 +1,27 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Providers.Plugins.MusicBrainz;
-namespace MediaBrowser.Providers.Music
+namespace MediaBrowser.Providers.Plugins.MusicBrainz;
+
+/// <summary>
+/// MusicBrainz other artist external id.
+/// </summary>
+public class MusicBrainzOtherArtistExternalId : IExternalId
{
- public class MusicBrainzOtherArtistExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "MusicBrainz";
+ /// <inheritdoc />
+ public string ProviderName => "MusicBrainz";
- /// <inheritdoc />
- public string Key => MetadataProvider.MusicBrainzArtist.ToString();
+ /// <inheritdoc />
+ public string Key => MetadataProvider.MusicBrainzArtist.ToString();
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
- /// <inheritdoc />
- public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
+ /// <inheritdoc />
+ public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
- }
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum;
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs
index acb652fe0..0baab9955 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs
@@ -1,28 +1,27 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Providers.Plugins.MusicBrainz;
-namespace MediaBrowser.Providers.Music
+namespace MediaBrowser.Providers.Plugins.MusicBrainz;
+
+/// <summary>
+/// MusicBrainz release group external id.
+/// </summary>
+public class MusicBrainzReleaseGroupExternalId : IExternalId
{
- public class MusicBrainzReleaseGroupExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "MusicBrainz";
+ /// <inheritdoc />
+ public string ProviderName => "MusicBrainz";
- /// <inheritdoc />
- public string Key => MetadataProvider.MusicBrainzReleaseGroup.ToString();
+ /// <inheritdoc />
+ public string Key => MetadataProvider.MusicBrainzReleaseGroup.ToString();
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup;
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup;
- /// <inheritdoc />
- public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/release-group/{0}";
+ /// <inheritdoc />
+ public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/release-group/{0}";
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
- }
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum;
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs
index 14805b9b7..5c974c411 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs
@@ -1,28 +1,27 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Providers.Plugins.MusicBrainz;
-namespace MediaBrowser.Providers.Music
+namespace MediaBrowser.Providers.Plugins.MusicBrainz;
+
+/// <summary>
+/// MusicBrainz track id.
+/// </summary>
+public class MusicBrainzTrackId : IExternalId
{
- public class MusicBrainzTrackId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "MusicBrainz";
+ /// <inheritdoc />
+ public string ProviderName => "MusicBrainz";
- /// <inheritdoc />
- public string Key => MetadataProvider.MusicBrainzTrack.ToString();
+ /// <inheritdoc />
+ public string Key => MetadataProvider.MusicBrainzTrack.ToString();
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.Track;
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.Track;
- /// <inheritdoc />
- public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/track/{0}";
+ /// <inheritdoc />
+ public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/track/{0}";
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is Audio;
- }
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Audio;
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs
index cfa10dd64..39cfd727f 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs
@@ -1,45 +1,64 @@
-#nullable disable
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
+using System.Net.Http.Headers;
+using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
+using MediaBrowser.Providers.Plugins.MusicBrainz.Configuration;
+using MetaBrainz.MusicBrainz;
+
+namespace MediaBrowser.Providers.Plugins.MusicBrainz;
-namespace MediaBrowser.Providers.Plugins.MusicBrainz
+/// <summary>
+/// Plugin instance.
+/// </summary>
+public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
{
- public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Plugin"/> class.
+ /// </summary>
+ /// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
+ /// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
+ /// <param name="applicationHost">Instance of the <see cref="IApplicationHost"/> interface.</param>
+ public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, IApplicationHost applicationHost)
+ : base(applicationPaths, xmlSerializer)
{
- public const string DefaultServer = "https://musicbrainz.org";
-
- public const long DefaultRateLimit = 2000u;
+ Instance = this;
- public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
- : base(applicationPaths, xmlSerializer)
- {
- Instance = this;
- }
+ // TODO: Change this to "JellyfinMusicBrainzPlugin" once we take it out of the server repo.
+ Query.DefaultUserAgent.Add(new ProductInfoHeaderValue(applicationHost.Name.Replace(' ', '-'), applicationHost.ApplicationVersionString));
+ Query.DefaultUserAgent.Add(new ProductInfoHeaderValue($"({applicationHost.ApplicationUserAgentAddress})"));
+ Query.DelayBetweenRequests = Instance.Configuration.RateLimit;
+ Query.DefaultServer = Instance.Configuration.Server;
+ }
- public static Plugin Instance { get; private set; }
+ /// <summary>
+ /// Gets the current plugin instance.
+ /// </summary>
+ public static Plugin? Instance { get; private set; }
- public override Guid Id => new Guid("8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a");
+ /// <inheritdoc />
+ public override Guid Id => new Guid("8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a");
- public override string Name => "MusicBrainz";
+ /// <inheritdoc />
+ public override string Name => "MusicBrainz";
- public override string Description => "Get artist and album metadata from any MusicBrainz server.";
+ /// <inheritdoc />
+ public override string Description => "Get artist and album metadata from any MusicBrainz server.";
- // TODO remove when plugin removed from server.
- public override string ConfigurationFileName => "Jellyfin.Plugin.MusicBrainz.xml";
+ /// <inheritdoc />
+ // TODO remove when plugin removed from server.
+ public override string ConfigurationFileName => "Jellyfin.Plugin.MusicBrainz.xml";
- public IEnumerable<PluginPageInfo> GetPages()
+ /// <inheritdoc />
+ public IEnumerable<PluginPageInfo> GetPages()
+ {
+ yield return new PluginPageInfo
{
- yield return new PluginPageInfo
- {
- Name = Name,
- EmbeddedResourcePath = GetType().Namespace + ".Configuration.config.html"
- };
- }
+ Name = Name,
+ EmbeddedResourcePath = GetType().Namespace + ".Configuration.config.html"
+ };
}
}
diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64
index 0bae42bc8..1bdef2d59 100644
--- a/deployment/Dockerfile.centos.amd64
+++ b/deployment/Dockerfile.centos.amd64
@@ -13,7 +13,7 @@ RUN yum update -yq \
&& yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/8159607a-e686-4ead-ac99-b4c97290a5fd/ec6070b1b2cc0651ebe57cf1bd411315/dotnet-sdk-6.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/d3e46476-4494-41b7-a628-c517794c5a6a/6066215f6c0a18b070e8e6e8b715de0b/dotnet-sdk-6.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64
index 20aa777b6..945bf8116 100644
--- a/deployment/Dockerfile.fedora.amd64
+++ b/deployment/Dockerfile.fedora.amd64
@@ -12,7 +12,7 @@ RUN dnf update -yq \
&& dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/8159607a-e686-4ead-ac99-b4c97290a5fd/ec6070b1b2cc0651ebe57cf1bd411315/dotnet-sdk-6.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/d3e46476-4494-41b7-a628-c517794c5a6a/6066215f6c0a18b070e8e6e8b715de0b/dotnet-sdk-6.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
index ccc0f76cd..a63cd6527 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -17,7 +17,7 @@ RUN apt-get update -yqq \
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/8159607a-e686-4ead-ac99-b4c97290a5fd/ec6070b1b2cc0651ebe57cf1bd411315/dotnet-sdk-6.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/d3e46476-4494-41b7-a628-c517794c5a6a/6066215f6c0a18b070e8e6e8b715de0b/dotnet-sdk-6.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64
index 893180974..2b9ea9bf6 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/8159607a-e686-4ead-ac99-b4c97290a5fd/ec6070b1b2cc0651ebe57cf1bd411315/dotnet-sdk-6.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/d3e46476-4494-41b7-a628-c517794c5a6a/6066215f6c0a18b070e8e6e8b715de0b/dotnet-sdk-6.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf
index bf1edf777..3d3e49af8 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/8159607a-e686-4ead-ac99-b4c97290a5fd/ec6070b1b2cc0651ebe57cf1bd411315/dotnet-sdk-6.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/d3e46476-4494-41b7-a628-c517794c5a6a/6066215f6c0a18b070e8e6e8b715de0b/dotnet-sdk-6.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
index 7cd98c29a..81c8f2ba9 100644
--- a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
+++ b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
@@ -18,7 +18,7 @@
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
- <PackageReference Include="Moq" Version="4.16.1" />
+ <PackageReference Include="Moq" Version="4.18.2" />
<PackageReference Include="SharpFuzz" Version="1.6.2" />
</ItemGroup>
diff --git a/jellyfin.ruleset b/jellyfin.ruleset
index 5ac5f4923..8144db93d 100644
--- a/jellyfin.ruleset
+++ b/jellyfin.ruleset
@@ -5,8 +5,16 @@
<Rule Id="SA1000" Action="Error" />
<!-- error on SA1001: Commas should not be preceded by whitespace -->
<Rule Id="SA1001" Action="Error" />
+ <!-- error on SA1106: Code should not contain empty statements -->
+ <Rule Id="SA1106" Action="Error" />
+ <!-- error on SA1107: Code should not contain multiple statements on one line -->
+ <Rule Id="SA1107" Action="Error" />
+ <!-- error on SA1028: Code should not contain trailing whitespace -->
+ <Rule Id="SA1028" Action="Error" />
<!-- error on SA1117: The parameters should all be placed on the same line or each parameter should be placed on its own line -->
<Rule Id="SA1117" Action="Error" />
+ <!-- error on SA1137: Elements should have the same indentation -->
+ <Rule Id="SA1137" Action="Error" />
<!-- error on SA1142: Refer to tuple fields by name -->
<Rule Id="SA1142" Action="Error" />
<!-- error on SA1210: Using directives should be ordered alphabetically by the namespaces -->
@@ -69,6 +77,8 @@
<Rule Id="CA1307" Action="Error" />
<!-- error on CA1309: Use ordinal StringComparison -->
<Rule Id="CA1309" Action="Error" />
+ <!-- error on CA1310: Specify StringComparison for correctness -->
+ <Rule Id="CA1310" Action="Error" />
<!-- error on CA1725: Parameter names should match base declaration -->
<Rule Id="CA1725" Action="Error" />
<!-- error on CA1725: Call async methods when in an async method -->
diff --git a/src/Jellyfin.Extensions/EnumerableExtensions.cs b/src/Jellyfin.Extensions/EnumerableExtensions.cs
index a31a57dc6..fd46358a4 100644
--- a/src/Jellyfin.Extensions/EnumerableExtensions.cs
+++ b/src/Jellyfin.Extensions/EnumerableExtensions.cs
@@ -1,42 +1,31 @@
using System;
using System.Collections.Generic;
-namespace Jellyfin.Extensions
+namespace Jellyfin.Extensions;
+
+/// <summary>
+/// Static extensions for the <see cref="IEnumerable{T}"/> interface.
+/// </summary>
+public static class EnumerableExtensions
{
/// <summary>
- /// Static extensions for the <see cref="IEnumerable{T}"/> interface.
+ /// Determines whether the value is contained in the source collection.
/// </summary>
- public static class EnumerableExtensions
+ /// <param name="source">An instance of the <see cref="IEnumerable{String}"/> interface.</param>
+ /// <param name="value">The value to look for in the collection.</param>
+ /// <param name="stringComparison">The string comparison.</param>
+ /// <returns>A value indicating whether the value is contained in the collection.</returns>
+ /// <exception cref="ArgumentNullException">The source is null.</exception>
+ public static bool Contains(this IEnumerable<string> source, ReadOnlySpan<char> value, StringComparison stringComparison)
{
- /// <summary>
- /// Determines whether the value is contained in the source collection.
- /// </summary>
- /// <param name="source">An instance of the <see cref="IEnumerable{String}"/> interface.</param>
- /// <param name="value">The value to look for in the collection.</param>
- /// <param name="stringComparison">The string comparison.</param>
- /// <returns>A value indicating whether the value is contained in the collection.</returns>
- /// <exception cref="ArgumentNullException">The source is null.</exception>
- public static bool Contains(this IEnumerable<string> source, ReadOnlySpan<char> value, StringComparison stringComparison)
- {
- ArgumentNullException.ThrowIfNull(source);
-
- if (source is IList<string> list)
- {
- int len = list.Count;
- for (int i = 0; i < len; i++)
- {
- if (value.Equals(list[i], stringComparison))
- {
- return true;
- }
- }
-
- return false;
- }
+ ArgumentNullException.ThrowIfNull(source);
- foreach (string element in source)
+ if (source is IList<string> list)
+ {
+ int len = list.Count;
+ for (int i = 0; i < len; i++)
{
- if (value.Equals(element, stringComparison))
+ if (value.Equals(list[i], stringComparison))
{
return true;
}
@@ -44,5 +33,26 @@ namespace Jellyfin.Extensions
return false;
}
+
+ foreach (string element in source)
+ {
+ if (value.Equals(element, stringComparison))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Gets an IEnumerable from a single item.
+ /// </summary>
+ /// <param name="item">The item to return.</param>
+ /// <typeparam name="T">The type of item.</typeparam>
+ /// <returns>The IEnumerable{T}.</returns>
+ public static IEnumerable<T> SingleItemAsEnumerable<T>(this T item)
+ {
+ yield return item;
}
}
diff --git a/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs b/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs
new file mode 100644
index 000000000..985bbcde1
--- /dev/null
+++ b/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs
@@ -0,0 +1,18 @@
+using MediaBrowser.Controller.Entities;
+using Xunit;
+
+namespace Jellyfin.Controller.Tests.Entities
+{
+ public class BaseItemTests
+ {
+ [Theory]
+ [InlineData("", "")]
+ [InlineData("1", "0000000001")]
+ [InlineData("t", "t")]
+ [InlineData("test", "test")]
+ [InlineData("test1", "test0000000001")]
+ [InlineData("1test 2", "0000000001test 0000000002")]
+ public void BaseItem_ModifySortChunks_Valid(string input, string expected)
+ => Assert.Equal(expected, BaseItem.ModifySortChunks(input));
+ }
+}
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs
index eea8cb50a..8f276d03f 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs
@@ -7,7 +7,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Providers.Music;
+using MediaBrowser.Providers.Plugins.MusicBrainz;
using MediaBrowser.XbmcMetadata.Parsers;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs
index 8ca3dd96e..78183d9ff 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs
@@ -7,7 +7,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Providers.Music;
+using MediaBrowser.Providers.Plugins.MusicBrainz;
using MediaBrowser.XbmcMetadata.Parsers;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;